blob: 4e3110d8f30df9471c9c5a99166dd9a15b93d125 [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
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +010026import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000027import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000028import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000029import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000030import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000031import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000032import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000033import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000034import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000035
36try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080037 import readline # pylint: disable=import-error,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000038except ImportError:
39 pass
40
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000041from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000042from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000043from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000044import auth
skobes6468b902016-10-24 08:45:10 -070045import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000046import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000047import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000048import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000049import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000050import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000051import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000052import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000053import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000054import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000055import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000056import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000057import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000059import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000060import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000061import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000062import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000063import watchlists
64
tandrii7400cf02016-06-21 08:48:07 -070065__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000066
tandrii9d2c7a32016-06-22 03:42:45 -070067COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070068DEFAULT_SERVER = 'https://codereview.chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080069POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000070DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +000071REFS_THAT_ALIAS_TO_OTHER_REFS = {
72 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
73 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
74}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000075
thestig@chromium.org44202a22014-03-11 19:22:18 +000076# Valid extensions for files we want to lint.
77DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
78DEFAULT_LINT_IGNORE_REGEX = r"$^"
79
borenet6c0efe62016-10-19 08:13:29 -070080# Buildbucket master name prefix.
81MASTER_PREFIX = 'master.'
82
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000083# Shortcut since it quickly becomes redundant.
84Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000085
maruel@chromium.orgddd59412011-11-30 14:20:38 +000086# Initialized in main()
87settings = None
88
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010089# Used by tests/git_cl_test.py to add extra logging.
90# Inside the weirdly failing test, add this:
91# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
92# And scroll up to see the strack trace printed.
93_IS_BEING_TESTED = False
94
maruel@chromium.orgddd59412011-11-30 14:20:38 +000095
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000096def DieWithError(message):
vapiera7fbd5a2016-06-16 09:17:49 -070097 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000098 sys.exit(1)
99
100
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000101def GetNoGitPagerEnv():
102 env = os.environ.copy()
103 # 'cat' is a magical git string that disables pagers on all platforms.
104 env['GIT_PAGER'] = 'cat'
105 return env
106
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000107
bsep@chromium.org627d9002016-04-29 00:00:52 +0000108def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000109 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000110 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000111 except subprocess2.CalledProcessError as e:
112 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000113 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000114 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000115 'Command "%s" failed.\n%s' % (
116 ' '.join(args), error_message or e.stdout or ''))
117 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000118
119
120def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000121 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000122 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000123
124
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000125def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000126 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700127 if suppress_stderr:
128 stderr = subprocess2.VOID
129 else:
130 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000131 try:
tandrii5d48c322016-08-18 16:19:37 -0700132 (out, _), code = subprocess2.communicate(['git'] + args,
133 env=GetNoGitPagerEnv(),
134 stdout=subprocess2.PIPE,
135 stderr=stderr)
136 return code, out
137 except subprocess2.CalledProcessError as e:
138 logging.debug('Failed running %s', args)
139 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000140
141
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000142def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000143 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000144 return RunGitWithCode(args, suppress_stderr=True)[1]
145
146
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000147def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000148 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000149 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000150 return (version.startswith(prefix) and
151 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000152
153
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000154def BranchExists(branch):
155 """Return True if specified branch exists."""
156 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
157 suppress_stderr=True)
158 return not code
159
160
tandrii2a16b952016-10-19 07:09:44 -0700161def time_sleep(seconds):
162 # Use this so that it can be mocked in tests without interfering with python
163 # system machinery.
164 import time # Local import to discourage others from importing time globally.
165 return time.sleep(seconds)
166
167
maruel@chromium.org90541732011-04-01 17:54:18 +0000168def ask_for_data(prompt):
169 try:
170 return raw_input(prompt)
171 except KeyboardInterrupt:
172 # Hide the exception.
173 sys.exit(1)
174
175
tandrii5d48c322016-08-18 16:19:37 -0700176def _git_branch_config_key(branch, key):
177 """Helper method to return Git config key for a branch."""
178 assert branch, 'branch name is required to set git config for it'
179 return 'branch.%s.%s' % (branch, key)
180
181
182def _git_get_branch_config_value(key, default=None, value_type=str,
183 branch=False):
184 """Returns git config value of given or current branch if any.
185
186 Returns default in all other cases.
187 """
188 assert value_type in (int, str, bool)
189 if branch is False: # Distinguishing default arg value from None.
190 branch = GetCurrentBranch()
191
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000192 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700193 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000194
tandrii5d48c322016-08-18 16:19:37 -0700195 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700196 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700197 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700198 # git config also has --int, but apparently git config suffers from integer
199 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700200 args.append(_git_branch_config_key(branch, key))
201 code, out = RunGitWithCode(args)
202 if code == 0:
203 value = out.strip()
204 if value_type == int:
205 return int(value)
206 if value_type == bool:
207 return bool(value.lower() == 'true')
208 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000209 return default
210
211
tandrii5d48c322016-08-18 16:19:37 -0700212def _git_set_branch_config_value(key, value, branch=None, **kwargs):
213 """Sets the value or unsets if it's None of a git branch config.
214
215 Valid, though not necessarily existing, branch must be provided,
216 otherwise currently checked out branch is used.
217 """
218 if not branch:
219 branch = GetCurrentBranch()
220 assert branch, 'a branch name OR currently checked out branch is required'
221 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700222 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700223 if value is None:
224 args.append('--unset')
225 elif isinstance(value, bool):
226 args.append('--bool')
227 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700228 else:
tandrii33a46ff2016-08-23 05:53:40 -0700229 # git config also has --int, but apparently git config suffers from integer
230 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700231 value = str(value)
232 args.append(_git_branch_config_key(branch, key))
233 if value is not None:
234 args.append(value)
235 RunGit(args, **kwargs)
236
237
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100238def _get_committer_timestamp(commit):
239 """Returns unix timestamp as integer of a committer in a commit.
240
241 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
242 """
243 # Git also stores timezone offset, but it only affects visual display,
244 # actual point in time is defined by this timestamp only.
245 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
246
247
248def _git_amend_head(message, committer_timestamp):
249 """Amends commit with new message and desired committer_timestamp.
250
251 Sets committer timezone to UTC.
252 """
253 env = os.environ.copy()
254 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
255 return RunGit(['commit', '--amend', '-m', message], env=env)
256
257
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000258def add_git_similarity(parser):
259 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700260 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000261 help='Sets the percentage that a pair of files need to match in order to'
262 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000263 parser.add_option(
264 '--find-copies', action='store_true',
265 help='Allows git to look for copies.')
266 parser.add_option(
267 '--no-find-copies', action='store_false', dest='find_copies',
268 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000269
270 old_parser_args = parser.parse_args
271 def Parse(args):
272 options, args = old_parser_args(args)
273
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000274 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700275 options.similarity = _git_get_branch_config_value(
276 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000277 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000278 print('Note: Saving similarity of %d%% in git config.'
279 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700280 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000281
iannucci@chromium.org79540052012-10-19 23:15:26 +0000282 options.similarity = max(0, min(options.similarity, 100))
283
284 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700285 options.find_copies = _git_get_branch_config_value(
286 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000287 else:
tandrii5d48c322016-08-18 16:19:37 -0700288 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000289
290 print('Using %d%% similarity for rename/copy detection. '
291 'Override with --similarity.' % options.similarity)
292
293 return options, args
294 parser.parse_args = Parse
295
296
machenbach@chromium.org45453142015-09-15 08:45:22 +0000297def _get_properties_from_options(options):
298 properties = dict(x.split('=', 1) for x in options.properties)
299 for key, val in properties.iteritems():
300 try:
301 properties[key] = json.loads(val)
302 except ValueError:
303 pass # If a value couldn't be evaluated, treat it as a string.
304 return properties
305
306
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000307def _prefix_master(master):
308 """Convert user-specified master name to full master name.
309
310 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
311 name, while the developers always use shortened master name
312 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
313 function does the conversion for buildbucket migration.
314 """
borenet6c0efe62016-10-19 08:13:29 -0700315 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000316 return master
borenet6c0efe62016-10-19 08:13:29 -0700317 return '%s%s' % (MASTER_PREFIX, master)
318
319
320def _unprefix_master(bucket):
321 """Convert bucket name to shortened master name.
322
323 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
324 name, while the developers always use shortened master name
325 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
326 function does the conversion for buildbucket migration.
327 """
328 if bucket.startswith(MASTER_PREFIX):
329 return bucket[len(MASTER_PREFIX):]
330 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000331
332
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000333def _buildbucket_retry(operation_name, http, *args, **kwargs):
334 """Retries requests to buildbucket service and returns parsed json content."""
335 try_count = 0
336 while True:
337 response, content = http.request(*args, **kwargs)
338 try:
339 content_json = json.loads(content)
340 except ValueError:
341 content_json = None
342
343 # Buildbucket could return an error even if status==200.
344 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000345 error = content_json.get('error')
346 if error.get('code') == 403:
347 raise BuildbucketResponseException(
348 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000349 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000350 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000351 raise BuildbucketResponseException(msg)
352
353 if response.status == 200:
354 if not content_json:
355 raise BuildbucketResponseException(
356 'Buildbucket returns invalid json content: %s.\n'
357 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
358 content)
359 return content_json
360 if response.status < 500 or try_count >= 2:
361 raise httplib2.HttpLib2Error(content)
362
363 # status >= 500 means transient failures.
364 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700365 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000366 try_count += 1
367 assert False, 'unreachable'
368
369
qyearsley1fdfcb62016-10-24 13:22:03 -0700370def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700371 """Returns a dict mapping bucket names to builders and tests,
372 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700373 """
qyearsleydd49f942016-10-28 11:57:22 -0700374 # If no bots are listed, we try to get a set of builders and tests based
375 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700376 if not options.bot:
377 change = changelist.GetChange(
378 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700379 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700380 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700381 change=change,
382 changed_files=change.LocalPaths(),
383 repository_root=settings.GetRoot(),
384 default_presubmit=None,
385 project=None,
386 verbose=options.verbose,
387 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700388 if masters is None:
389 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100390 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700391
qyearsley1fdfcb62016-10-24 13:22:03 -0700392 if options.bucket:
393 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700394 if options.master:
395 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700396
qyearsleydd49f942016-10-28 11:57:22 -0700397 # If bots are listed but no master or bucket, then we need to find out
398 # the corresponding master for each bot.
399 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
400 if error_message:
401 option_parser.error(
402 'Tryserver master cannot be found because: %s\n'
403 'Please manually specify the tryserver master, e.g. '
404 '"-m tryserver.chromium.linux".' % error_message)
405 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700406
407
qyearsley123a4682016-10-26 09:12:17 -0700408def _get_bucket_map_for_builders(builders):
409 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700410 map_url = 'https://builders-map.appspot.com/'
411 try:
qyearsley123a4682016-10-26 09:12:17 -0700412 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700413 except urllib2.URLError as e:
414 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
415 (map_url, e))
416 except ValueError as e:
417 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700418 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700419 return None, 'Failed to build master map.'
420
qyearsley123a4682016-10-26 09:12:17 -0700421 bucket_map = {}
422 for builder in builders:
qyearsley123a4682016-10-26 09:12:17 -0700423 masters = builders_map.get(builder, [])
424 if not masters:
qyearsley1fdfcb62016-10-24 13:22:03 -0700425 return None, ('No matching master for builder %s.' % builder)
qyearsley123a4682016-10-26 09:12:17 -0700426 if len(masters) > 1:
qyearsley1fdfcb62016-10-24 13:22:03 -0700427 return None, ('The builder name %s exists in multiple masters %s.' %
qyearsley123a4682016-10-26 09:12:17 -0700428 (builder, masters))
429 bucket = _prefix_master(masters[0])
430 bucket_map.setdefault(bucket, {})[builder] = []
431
432 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700433
434
borenet6c0efe62016-10-19 08:13:29 -0700435def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700436 category='git_cl_try', patchset=None):
qyearsley1fdfcb62016-10-24 13:22:03 -0700437 """Sends a request to Buildbucket to trigger try jobs for a changelist.
438
439 Args:
440 auth_config: AuthConfig for Rietveld.
441 changelist: Changelist that the try jobs are associated with.
442 buckets: A nested dict mapping bucket names to builders to tests.
443 options: Command-line options.
444 """
tandriide281ae2016-10-12 06:02:30 -0700445 assert changelist.GetIssue(), 'CL must be uploaded first'
446 codereview_url = changelist.GetCodereviewServer()
447 assert codereview_url, 'CL must be uploaded first'
448 patchset = patchset or changelist.GetMostRecentPatchset()
449 assert patchset, 'CL must be uploaded first'
450
451 codereview_host = urlparse.urlparse(codereview_url).hostname
452 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000453 http = authenticator.authorize(httplib2.Http())
454 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700455
456 # TODO(tandrii): consider caching Gerrit CL details just like
457 # _RietveldChangelistImpl does, then caching values in these two variables
458 # won't be necessary.
459 owner_email = changelist.GetIssueOwner()
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000460
461 buildbucket_put_url = (
462 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000463 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700464 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
465 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
466 hostname=codereview_host,
467 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000468 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700469
470 shared_parameters_properties = changelist.GetTryjobProperties(patchset)
471 shared_parameters_properties['category'] = category
472 if options.clobber:
473 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700474 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700475 if extra_properties:
476 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000477
478 batch_req_body = {'builds': []}
479 print_text = []
480 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700481 for bucket, builders_and_tests in sorted(buckets.iteritems()):
482 print_text.append('Bucket: %s' % bucket)
483 master = None
484 if bucket.startswith(MASTER_PREFIX):
485 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000486 for builder, tests in sorted(builders_and_tests.iteritems()):
487 print_text.append(' %s: %s' % (builder, tests))
488 parameters = {
489 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000490 'changes': [{
tandriide281ae2016-10-12 06:02:30 -0700491 'author': {'email': owner_email},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000492 'revision': options.revision,
493 }],
tandrii8c5a3532016-11-04 07:52:02 -0700494 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000495 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000496 if 'presubmit' in builder.lower():
497 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000498 if tests:
499 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700500
501 tags = [
502 'builder:%s' % builder,
503 'buildset:%s' % buildset,
504 'user_agent:git_cl_try',
505 ]
506 if master:
507 parameters['properties']['master'] = master
508 tags.append('master:%s' % master)
509
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000510 batch_req_body['builds'].append(
511 {
512 'bucket': bucket,
513 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000514 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700515 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000516 }
517 )
518
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000519 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700520 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000521 http,
522 buildbucket_put_url,
523 'PUT',
524 body=json.dumps(batch_req_body),
525 headers={'Content-Type': 'application/json'}
526 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000527 print_text.append('To see results here, run: git cl try-results')
528 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700529 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000530
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000531
tandrii221ab252016-10-06 08:12:04 -0700532def fetch_try_jobs(auth_config, changelist, buildbucket_host,
533 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700534 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000535
qyearsley53f48a12016-09-01 10:45:13 -0700536 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000537 """
tandrii221ab252016-10-06 08:12:04 -0700538 assert buildbucket_host
539 assert changelist.GetIssue(), 'CL must be uploaded first'
540 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
541 patchset = patchset or changelist.GetMostRecentPatchset()
542 assert patchset, 'CL must be uploaded first'
543
544 codereview_url = changelist.GetCodereviewServer()
545 codereview_host = urlparse.urlparse(codereview_url).hostname
546 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000547 if authenticator.has_cached_credentials():
548 http = authenticator.authorize(httplib2.Http())
549 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700550 print('Warning: Some results might be missing because %s' %
551 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700552 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000553 http = httplib2.Http()
554
555 http.force_exception_to_status_code = True
556
tandrii221ab252016-10-06 08:12:04 -0700557 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
558 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
559 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000560 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700561 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000562 params = {'tag': 'buildset:%s' % buildset}
563
564 builds = {}
565 while True:
566 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700567 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000568 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700569 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000570 for build in content.get('builds', []):
571 builds[build['id']] = build
572 if 'next_cursor' in content:
573 params['start_cursor'] = content['next_cursor']
574 else:
575 break
576 return builds
577
578
qyearsleyeab3c042016-08-24 09:18:28 -0700579def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000580 """Prints nicely result of fetch_try_jobs."""
581 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700582 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000583 return
584
585 # Make a copy, because we'll be modifying builds dictionary.
586 builds = builds.copy()
587 builder_names_cache = {}
588
589 def get_builder(b):
590 try:
591 return builder_names_cache[b['id']]
592 except KeyError:
593 try:
594 parameters = json.loads(b['parameters_json'])
595 name = parameters['builder_name']
596 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700597 print('WARNING: failed to get builder name for build %s: %s' % (
598 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000599 name = None
600 builder_names_cache[b['id']] = name
601 return name
602
603 def get_bucket(b):
604 bucket = b['bucket']
605 if bucket.startswith('master.'):
606 return bucket[len('master.'):]
607 return bucket
608
609 if options.print_master:
610 name_fmt = '%%-%ds %%-%ds' % (
611 max(len(str(get_bucket(b))) for b in builds.itervalues()),
612 max(len(str(get_builder(b))) for b in builds.itervalues()))
613 def get_name(b):
614 return name_fmt % (get_bucket(b), get_builder(b))
615 else:
616 name_fmt = '%%-%ds' % (
617 max(len(str(get_builder(b))) for b in builds.itervalues()))
618 def get_name(b):
619 return name_fmt % get_builder(b)
620
621 def sort_key(b):
622 return b['status'], b.get('result'), get_name(b), b.get('url')
623
624 def pop(title, f, color=None, **kwargs):
625 """Pop matching builds from `builds` dict and print them."""
626
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000627 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000628 colorize = str
629 else:
630 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
631
632 result = []
633 for b in builds.values():
634 if all(b.get(k) == v for k, v in kwargs.iteritems()):
635 builds.pop(b['id'])
636 result.append(b)
637 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700638 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000639 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700640 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000641
642 total = len(builds)
643 pop(status='COMPLETED', result='SUCCESS',
644 title='Successes:', color=Fore.GREEN,
645 f=lambda b: (get_name(b), b.get('url')))
646 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
647 title='Infra Failures:', color=Fore.MAGENTA,
648 f=lambda b: (get_name(b), b.get('url')))
649 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
650 title='Failures:', color=Fore.RED,
651 f=lambda b: (get_name(b), b.get('url')))
652 pop(status='COMPLETED', result='CANCELED',
653 title='Canceled:', color=Fore.MAGENTA,
654 f=lambda b: (get_name(b),))
655 pop(status='COMPLETED', result='FAILURE',
656 failure_reason='INVALID_BUILD_DEFINITION',
657 title='Wrong master/builder name:', color=Fore.MAGENTA,
658 f=lambda b: (get_name(b),))
659 pop(status='COMPLETED', result='FAILURE',
660 title='Other failures:',
661 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
662 pop(status='COMPLETED',
663 title='Other finished:',
664 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
665 pop(status='STARTED',
666 title='Started:', color=Fore.YELLOW,
667 f=lambda b: (get_name(b), b.get('url')))
668 pop(status='SCHEDULED',
669 title='Scheduled:',
670 f=lambda b: (get_name(b), 'id=%s' % b['id']))
671 # The last section is just in case buildbucket API changes OR there is a bug.
672 pop(title='Other:',
673 f=lambda b: (get_name(b), 'id=%s' % b['id']))
674 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700675 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000676
677
qyearsley53f48a12016-09-01 10:45:13 -0700678def write_try_results_json(output_file, builds):
679 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
680
681 The input |builds| dict is assumed to be generated by Buildbucket.
682 Buildbucket documentation: http://goo.gl/G0s101
683 """
684
685 def convert_build_dict(build):
686 return {
687 'buildbucket_id': build.get('id'),
688 'status': build.get('status'),
689 'result': build.get('result'),
690 'bucket': build.get('bucket'),
691 'builder_name': json.loads(
692 build.get('parameters_json', '{}')).get('builder_name'),
693 'failure_reason': build.get('failure_reason'),
694 'url': build.get('url'),
695 }
696
697 converted = []
698 for _, build in sorted(builds.items()):
699 converted.append(convert_build_dict(build))
700 write_json(output_file, converted)
701
702
iannucci@chromium.org79540052012-10-19 23:15:26 +0000703def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000704 """Prints statistics about the change to the user."""
705 # --no-ext-diff is broken in some versions of Git, so try to work around
706 # this by overriding the environment (but there is still a problem if the
707 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000708 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000709 if 'GIT_EXTERNAL_DIFF' in env:
710 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000711
712 if find_copies:
scottmgb84b5e32016-11-10 09:25:33 -0800713 similarity_options = ['-l100000', '-C%s' % similarity]
iannucci@chromium.org79540052012-10-19 23:15:26 +0000714 else:
715 similarity_options = ['-M%s' % similarity]
716
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000717 try:
718 stdout = sys.stdout.fileno()
719 except AttributeError:
720 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000721 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000722 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000723 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000724 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000725
726
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000727class BuildbucketResponseException(Exception):
728 pass
729
730
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000731class Settings(object):
732 def __init__(self):
733 self.default_server = None
734 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000735 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000736 self.tree_status_url = None
737 self.viewvc_url = None
738 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000739 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000740 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000741 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000742 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000743 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000744 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000745 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000746
747 def LazyUpdateIfNeeded(self):
748 """Updates the settings from a codereview.settings file, if available."""
749 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000750 # The only value that actually changes the behavior is
751 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000752 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000753 error_ok=True
754 ).strip().lower()
755
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000756 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000757 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000758 LoadCodereviewSettingsFromFile(cr_settings_file)
759 self.updated = True
760
761 def GetDefaultServerUrl(self, error_ok=False):
762 if not self.default_server:
763 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000764 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000765 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000766 if error_ok:
767 return self.default_server
768 if not self.default_server:
769 error_message = ('Could not find settings file. You must configure '
770 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000771 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000772 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000773 return self.default_server
774
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000775 @staticmethod
776 def GetRelativeRoot():
777 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000778
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000779 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000780 if self.root is None:
781 self.root = os.path.abspath(self.GetRelativeRoot())
782 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000783
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000784 def GetGitMirror(self, remote='origin'):
785 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000786 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000787 if not os.path.isdir(local_url):
788 return None
789 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
790 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
791 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
792 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
793 if mirror.exists():
794 return mirror
795 return None
796
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000797 def GetTreeStatusUrl(self, error_ok=False):
798 if not self.tree_status_url:
799 error_message = ('You must configure your tree status URL by running '
800 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000801 self.tree_status_url = self._GetRietveldConfig(
802 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000803 return self.tree_status_url
804
805 def GetViewVCUrl(self):
806 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000807 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000808 return self.viewvc_url
809
rmistry@google.com90752582014-01-14 21:04:50 +0000810 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000811 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000812
rmistry@google.com78948ed2015-07-08 23:09:57 +0000813 def GetIsSkipDependencyUpload(self, branch_name):
814 """Returns true if specified branch should skip dep uploads."""
815 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
816 error_ok=True)
817
rmistry@google.com5626a922015-02-26 14:03:30 +0000818 def GetRunPostUploadHook(self):
819 run_post_upload_hook = self._GetRietveldConfig(
820 'run-post-upload-hook', error_ok=True)
821 return run_post_upload_hook == "True"
822
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000823 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000824 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000825
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000826 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000827 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000828
ukai@chromium.orge8077812012-02-03 03:41:46 +0000829 def GetIsGerrit(self):
830 """Return true if this repo is assosiated with gerrit code review system."""
831 if self.is_gerrit is None:
832 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
833 return self.is_gerrit
834
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000835 def GetSquashGerritUploads(self):
836 """Return true if uploads to Gerrit should be squashed by default."""
837 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700838 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
839 if self.squash_gerrit_uploads is None:
840 # Default is squash now (http://crbug.com/611892#c23).
841 self.squash_gerrit_uploads = not (
842 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
843 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000844 return self.squash_gerrit_uploads
845
tandriia60502f2016-06-20 02:01:53 -0700846 def GetSquashGerritUploadsOverride(self):
847 """Return True or False if codereview.settings should be overridden.
848
849 Returns None if no override has been defined.
850 """
851 # See also http://crbug.com/611892#c23
852 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
853 error_ok=True).strip()
854 if result == 'true':
855 return True
856 if result == 'false':
857 return False
858 return None
859
tandrii@chromium.org28253532016-04-14 13:46:56 +0000860 def GetGerritSkipEnsureAuthenticated(self):
861 """Return True if EnsureAuthenticated should not be done for Gerrit
862 uploads."""
863 if self.gerrit_skip_ensure_authenticated is None:
864 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000865 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000866 error_ok=True).strip() == 'true')
867 return self.gerrit_skip_ensure_authenticated
868
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000869 def GetGitEditor(self):
870 """Return the editor specified in the git config, or None if none is."""
871 if self.git_editor is None:
872 self.git_editor = self._GetConfig('core.editor', error_ok=True)
873 return self.git_editor or None
874
thestig@chromium.org44202a22014-03-11 19:22:18 +0000875 def GetLintRegex(self):
876 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
877 DEFAULT_LINT_REGEX)
878
879 def GetLintIgnoreRegex(self):
880 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
881 DEFAULT_LINT_IGNORE_REGEX)
882
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000883 def GetProject(self):
884 if not self.project:
885 self.project = self._GetRietveldConfig('project', error_ok=True)
886 return self.project
887
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000888 def GetForceHttpsCommitUrl(self):
889 if not self.force_https_commit_url:
890 self.force_https_commit_url = self._GetRietveldConfig(
891 'force-https-commit-url', error_ok=True)
892 return self.force_https_commit_url
893
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000894 def GetPendingRefPrefix(self):
895 if not self.pending_ref_prefix:
896 self.pending_ref_prefix = self._GetRietveldConfig(
897 'pending-ref-prefix', error_ok=True)
898 return self.pending_ref_prefix
899
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000900 def _GetRietveldConfig(self, param, **kwargs):
901 return self._GetConfig('rietveld.' + param, **kwargs)
902
rmistry@google.com78948ed2015-07-08 23:09:57 +0000903 def _GetBranchConfig(self, branch_name, param, **kwargs):
904 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
905
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000906 def _GetConfig(self, param, **kwargs):
907 self.LazyUpdateIfNeeded()
908 return RunGit(['config', param], **kwargs).strip()
909
910
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100911class _GitNumbererState(object):
912 KNOWN_PROJECTS_WHITELIST = [
913 'chromium/src',
914 'external/webrtc',
915 'v8/v8',
916 ]
917
918 @classmethod
919 def load(cls, remote_url, remote_ref):
920 """Figures out the state by fetching special refs from remote repo.
921 """
922 assert remote_ref and remote_ref.startswith('refs/'), remote_ref
923 url_parts = urlparse.urlparse(remote_url)
924 project_name = url_parts.path.lstrip('/').rstrip('git./')
925 for known in cls.KNOWN_PROJECTS_WHITELIST:
926 if project_name.endswith(known):
927 break
928 else:
929 # Early exit to avoid extra fetches for repos that aren't using gnumbd.
930 return cls(cls._get_pending_prefix_fallback(), None)
931
Quinten Yearsley442fb642016-12-15 15:38:27 -0800932 # This pollutes local ref space, but the amount of objects is negligible.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100933 error, _ = cls._run_git_with_code([
934 'fetch', remote_url,
935 '+refs/meta/config:refs/git_cl/meta/config',
936 '+refs/gnumbd-config/main:refs/git_cl/gnumbd-config/main'])
937 if error:
938 # Some ref doesn't exist or isn't accessible to current user.
939 # This shouldn't happen on production KNOWN_PROJECTS_WHITELIST
940 # with git-numberer.
941 cls._warn('failed to fetch gnumbd and project config for %s: %s',
942 remote_url, error)
943 return cls(cls._get_pending_prefix_fallback(), None)
944 return cls(cls._get_pending_prefix(remote_ref),
945 cls._is_validator_enabled(remote_ref))
946
947 @classmethod
948 def _get_pending_prefix(cls, ref):
949 error, gnumbd_config_data = cls._run_git_with_code(
950 ['show', 'refs/git_cl/gnumbd-config/main:config.json'])
951 if error:
952 cls._warn('gnumbd config file not found')
953 return cls._get_pending_prefix_fallback()
954
955 try:
956 config = json.loads(gnumbd_config_data)
957 if cls.match_refglobs(ref, config['enabled_refglobs']):
958 return config['pending_ref_prefix']
959 return None
960 except KeyboardInterrupt:
961 raise
962 except Exception as e:
963 cls._warn('failed to parse gnumbd config: %s', e)
964 return cls._get_pending_prefix_fallback()
965
966 @staticmethod
967 def _get_pending_prefix_fallback():
968 global settings
969 if not settings:
970 settings = Settings()
971 return settings.GetPendingRefPrefix()
972
973 @classmethod
974 def _is_validator_enabled(cls, ref):
975 error, project_config_data = cls._run_git_with_code(
976 ['show', 'refs/git_cl/meta/config:project.config'])
977 if error:
978 cls._warn('project.config file not found')
979 return False
980 # Gerrit's project.config is really a git config file.
981 # So, parse it as such.
982 with tempfile.NamedTemporaryFile(prefix='git_cl_proj_config') as f:
983 f.write(project_config_data)
984 # Make sure OS sees this, but don't close the file just yet,
985 # as NamedTemporaryFile deletes it on closing.
986 f.flush()
987
988 def get_opts(x):
989 code, out = cls._run_git_with_code(
990 ['config', '-f', f.name, '--get-all',
991 'plugin.git-numberer.validate-%s-refglob' % x])
992 if code == 0:
993 return out.strip().splitlines()
994 return []
995 enabled, disabled = map(get_opts, ['enabled', 'disabled'])
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100996 logging.info('validator config enabled %s disabled %s refglobs for '
997 '(this ref: %s)', enabled, disabled, ref)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100998
999 if cls.match_refglobs(ref, disabled):
1000 return False
1001 return cls.match_refglobs(ref, enabled)
1002
1003 @staticmethod
1004 def match_refglobs(ref, refglobs):
1005 for refglob in refglobs:
1006 if ref == refglob or fnmatch.fnmatch(ref, refglob):
1007 return True
1008 return False
1009
1010 @staticmethod
1011 def _run_git_with_code(*args, **kwargs):
1012 # The only reason for this wrapper is easy porting of this code to CQ
1013 # codebase, which forked git_cl.py and checkouts.py long time ago.
1014 return RunGitWithCode(*args, **kwargs)
1015
1016 @staticmethod
1017 def _warn(msg, *args):
1018 if args:
1019 msg = msg % args
1020 print('WARNING: %s' % msg)
1021
1022 def __init__(self, pending_prefix, validator_enabled):
1023 # TODO(tandrii): remove pending_prefix after gnumbd is no more.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01001024 if pending_prefix:
1025 if not pending_prefix.endswith('/'):
1026 pending_prefix += '/'
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001027 self._pending_prefix = pending_prefix or None
1028 self._validator_enabled = validator_enabled or False
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01001029 logging.debug('_GitNumbererState(pending: %s, validator: %s)',
1030 self._pending_prefix, self._validator_enabled)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001031
1032 @property
1033 def pending_prefix(self):
1034 return self._pending_prefix
1035
1036 @property
Andrii Shyshkalov8f15f3e2016-12-14 15:43:49 +01001037 def should_add_git_number(self):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001038 return self._validator_enabled and self._pending_prefix is None
1039
1040
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001041def ShortBranchName(branch):
1042 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001043 return branch.replace('refs/heads/', '', 1)
1044
1045
1046def GetCurrentBranchRef():
1047 """Returns branch ref (e.g., refs/heads/master) or None."""
1048 return RunGit(['symbolic-ref', 'HEAD'],
1049 stderr=subprocess2.VOID, error_ok=True).strip() or None
1050
1051
1052def GetCurrentBranch():
1053 """Returns current branch or None.
1054
1055 For refs/heads/* branches, returns just last part. For others, full ref.
1056 """
1057 branchref = GetCurrentBranchRef()
1058 if branchref:
1059 return ShortBranchName(branchref)
1060 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001061
1062
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001063class _CQState(object):
1064 """Enum for states of CL with respect to Commit Queue."""
1065 NONE = 'none'
1066 DRY_RUN = 'dry_run'
1067 COMMIT = 'commit'
1068
1069 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1070
1071
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001072class _ParsedIssueNumberArgument(object):
1073 def __init__(self, issue=None, patchset=None, hostname=None):
1074 self.issue = issue
1075 self.patchset = patchset
1076 self.hostname = hostname
1077
1078 @property
1079 def valid(self):
1080 return self.issue is not None
1081
1082
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001083def ParseIssueNumberArgument(arg):
1084 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1085 fail_result = _ParsedIssueNumberArgument()
1086
1087 if arg.isdigit():
1088 return _ParsedIssueNumberArgument(issue=int(arg))
1089 if not arg.startswith('http'):
1090 return fail_result
1091 url = gclient_utils.UpgradeToHttps(arg)
1092 try:
1093 parsed_url = urlparse.urlparse(url)
1094 except ValueError:
1095 return fail_result
1096 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
1097 tmp = cls.ParseIssueURL(parsed_url)
1098 if tmp is not None:
1099 return tmp
1100 return fail_result
1101
1102
Aaron Gablea45ee112016-11-22 15:14:38 -08001103class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001104 def __init__(self, issue, url):
1105 self.issue = issue
1106 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001107 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001108
1109 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001110 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001111 self.issue, self.url)
1112
1113
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001114class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001115 """Changelist works with one changelist in local branch.
1116
1117 Supports two codereview backends: Rietveld or Gerrit, selected at object
1118 creation.
1119
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001120 Notes:
1121 * Not safe for concurrent multi-{thread,process} use.
1122 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001123 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001124 """
1125
1126 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1127 """Create a new ChangeList instance.
1128
1129 If issue is given, the codereview must be given too.
1130
1131 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1132 Otherwise, it's decided based on current configuration of the local branch,
1133 with default being 'rietveld' for backwards compatibility.
1134 See _load_codereview_impl for more details.
1135
1136 **kwargs will be passed directly to codereview implementation.
1137 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001138 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001139 global settings
1140 if not settings:
1141 # Happens when git_cl.py is used as a utility library.
1142 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001143
1144 if issue:
1145 assert codereview, 'codereview must be known, if issue is known'
1146
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001147 self.branchref = branchref
1148 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001149 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001150 self.branch = ShortBranchName(self.branchref)
1151 else:
1152 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001153 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001154 self.lookedup_issue = False
1155 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001156 self.has_description = False
1157 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001158 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001159 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001160 self.cc = None
1161 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001162 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001163
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001164 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001165 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001166 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001167 assert self._codereview_impl
1168 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001169
1170 def _load_codereview_impl(self, codereview=None, **kwargs):
1171 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001172 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1173 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1174 self._codereview = codereview
1175 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001176 return
1177
1178 # Automatic selection based on issue number set for a current branch.
1179 # Rietveld takes precedence over Gerrit.
1180 assert not self.issue
1181 # Whether we find issue or not, we are doing the lookup.
1182 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001183 if self.GetBranch():
1184 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1185 issue = _git_get_branch_config_value(
1186 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1187 if issue:
1188 self._codereview = codereview
1189 self._codereview_impl = cls(self, **kwargs)
1190 self.issue = int(issue)
1191 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001192
1193 # No issue is set for this branch, so decide based on repo-wide settings.
1194 return self._load_codereview_impl(
1195 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1196 **kwargs)
1197
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001198 def IsGerrit(self):
1199 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001200
1201 def GetCCList(self):
1202 """Return the users cc'd on this CL.
1203
agable92bec4f2016-08-24 09:27:27 -07001204 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001205 """
1206 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001207 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001208 more_cc = ','.join(self.watchers)
1209 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1210 return self.cc
1211
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001212 def GetCCListWithoutDefault(self):
1213 """Return the users cc'd on this CL excluding default ones."""
1214 if self.cc is None:
1215 self.cc = ','.join(self.watchers)
1216 return self.cc
1217
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001218 def SetWatchers(self, watchers):
1219 """Set the list of email addresses that should be cc'd based on the changed
1220 files in this CL.
1221 """
1222 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001223
1224 def GetBranch(self):
1225 """Returns the short branch name, e.g. 'master'."""
1226 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001227 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001228 if not branchref:
1229 return None
1230 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001231 self.branch = ShortBranchName(self.branchref)
1232 return self.branch
1233
1234 def GetBranchRef(self):
1235 """Returns the full branch name, e.g. 'refs/heads/master'."""
1236 self.GetBranch() # Poke the lazy loader.
1237 return self.branchref
1238
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001239 def ClearBranch(self):
1240 """Clears cached branch data of this object."""
1241 self.branch = self.branchref = None
1242
tandrii5d48c322016-08-18 16:19:37 -07001243 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1244 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1245 kwargs['branch'] = self.GetBranch()
1246 return _git_get_branch_config_value(key, default, **kwargs)
1247
1248 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1249 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1250 assert self.GetBranch(), (
1251 'this CL must have an associated branch to %sset %s%s' %
1252 ('un' if value is None else '',
1253 key,
1254 '' if value is None else ' to %r' % value))
1255 kwargs['branch'] = self.GetBranch()
1256 return _git_set_branch_config_value(key, value, **kwargs)
1257
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001258 @staticmethod
1259 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001260 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001261 e.g. 'origin', 'refs/heads/master'
1262 """
1263 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001264 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1265
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001266 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001267 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001268 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001269 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1270 error_ok=True).strip()
1271 if upstream_branch:
1272 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001273 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001274 # Else, try to guess the origin remote.
1275 remote_branches = RunGit(['branch', '-r']).split()
1276 if 'origin/master' in remote_branches:
1277 # Fall back on origin/master if it exits.
1278 remote = 'origin'
1279 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001280 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001281 DieWithError(
1282 'Unable to determine default branch to diff against.\n'
1283 'Either pass complete "git diff"-style arguments, like\n'
1284 ' git cl upload origin/master\n'
1285 'or verify this branch is set up to track another \n'
1286 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001287
1288 return remote, upstream_branch
1289
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001290 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001291 upstream_branch = self.GetUpstreamBranch()
1292 if not BranchExists(upstream_branch):
1293 DieWithError('The upstream for the current branch (%s) does not exist '
1294 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001295 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001296 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001297
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001298 def GetUpstreamBranch(self):
1299 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001300 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001301 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001302 upstream_branch = upstream_branch.replace('refs/heads/',
1303 'refs/remotes/%s/' % remote)
1304 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1305 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001306 self.upstream_branch = upstream_branch
1307 return self.upstream_branch
1308
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001309 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001310 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001311 remote, branch = None, self.GetBranch()
1312 seen_branches = set()
1313 while branch not in seen_branches:
1314 seen_branches.add(branch)
1315 remote, branch = self.FetchUpstreamTuple(branch)
1316 branch = ShortBranchName(branch)
1317 if remote != '.' or branch.startswith('refs/remotes'):
1318 break
1319 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001320 remotes = RunGit(['remote'], error_ok=True).split()
1321 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001322 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001323 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001324 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001325 logging.warn('Could not determine which remote this change is '
1326 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001327 else:
1328 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001329 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001330 branch = 'HEAD'
1331 if branch.startswith('refs/remotes'):
1332 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001333 elif branch.startswith('refs/branch-heads/'):
1334 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001335 else:
1336 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001337 return self._remote
1338
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001339 def GitSanityChecks(self, upstream_git_obj):
1340 """Checks git repo status and ensures diff is from local commits."""
1341
sbc@chromium.org79706062015-01-14 21:18:12 +00001342 if upstream_git_obj is None:
1343 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001344 print('ERROR: unable to determine current branch (detached HEAD?)',
1345 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001346 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001347 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001348 return False
1349
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001350 # Verify the commit we're diffing against is in our current branch.
1351 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1352 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1353 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001354 print('ERROR: %s is not in the current branch. You may need to rebase '
1355 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001356 return False
1357
1358 # List the commits inside the diff, and verify they are all local.
1359 commits_in_diff = RunGit(
1360 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1361 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1362 remote_branch = remote_branch.strip()
1363 if code != 0:
1364 _, remote_branch = self.GetRemoteBranch()
1365
1366 commits_in_remote = RunGit(
1367 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1368
1369 common_commits = set(commits_in_diff) & set(commits_in_remote)
1370 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001371 print('ERROR: Your diff contains %d commits already in %s.\n'
1372 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1373 'the diff. If you are using a custom git flow, you can override'
1374 ' the reference used for this check with "git config '
1375 'gitcl.remotebranch <git-ref>".' % (
1376 len(common_commits), remote_branch, upstream_git_obj),
1377 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001378 return False
1379 return True
1380
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001381 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001382 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001383
1384 Returns None if it is not set.
1385 """
tandrii5d48c322016-08-18 16:19:37 -07001386 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001387
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001388 def GetRemoteUrl(self):
1389 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1390
1391 Returns None if there is no remote.
1392 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001393 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001394 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1395
1396 # If URL is pointing to a local directory, it is probably a git cache.
1397 if os.path.isdir(url):
1398 url = RunGit(['config', 'remote.%s.url' % remote],
1399 error_ok=True,
1400 cwd=url).strip()
1401 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001402
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001403 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001404 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001405 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001406 self.issue = self._GitGetBranchConfigValue(
1407 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001408 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001409 return self.issue
1410
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001411 def GetIssueURL(self):
1412 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001413 issue = self.GetIssue()
1414 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001415 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001416 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001417
1418 def GetDescription(self, pretty=False):
1419 if not self.has_description:
1420 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001421 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001422 self.has_description = True
1423 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001424 # Set width to 72 columns + 2 space indent.
1425 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001426 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001427 lines = self.description.splitlines()
1428 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001429 return self.description
1430
1431 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001432 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001433 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001434 self.patchset = self._GitGetBranchConfigValue(
1435 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001436 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001437 return self.patchset
1438
1439 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001440 """Set this branch's patchset. If patchset=0, clears the patchset."""
1441 assert self.GetBranch()
1442 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001443 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001444 else:
1445 self.patchset = int(patchset)
1446 self._GitSetBranchConfigValue(
1447 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001448
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001449 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001450 """Set this branch's issue. If issue isn't given, clears the issue."""
1451 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001452 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001453 issue = int(issue)
1454 self._GitSetBranchConfigValue(
1455 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001456 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001457 codereview_server = self._codereview_impl.GetCodereviewServer()
1458 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001459 self._GitSetBranchConfigValue(
1460 self._codereview_impl.CodereviewServerConfigKey(),
1461 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001462 else:
tandrii5d48c322016-08-18 16:19:37 -07001463 # Reset all of these just to be clean.
1464 reset_suffixes = [
1465 'last-upload-hash',
1466 self._codereview_impl.IssueConfigKey(),
1467 self._codereview_impl.PatchsetConfigKey(),
1468 self._codereview_impl.CodereviewServerConfigKey(),
1469 ] + self._PostUnsetIssueProperties()
1470 for prop in reset_suffixes:
1471 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001472 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001473 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001474
dnjba1b0f32016-09-02 12:37:42 -07001475 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001476 if not self.GitSanityChecks(upstream_branch):
1477 DieWithError('\nGit sanity check failure')
1478
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001479 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001480 if not root:
1481 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001482 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001483
1484 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001485 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001486 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001487 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001488 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001489 except subprocess2.CalledProcessError:
1490 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001491 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001492 'This branch probably doesn\'t exist anymore. To reset the\n'
1493 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001494 ' git branch --set-upstream-to origin/master %s\n'
1495 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001496 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001497
maruel@chromium.org52424302012-08-29 15:14:30 +00001498 issue = self.GetIssue()
1499 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001500 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001501 description = self.GetDescription()
1502 else:
1503 # If the change was never uploaded, use the log messages of all commits
1504 # up to the branch point, as git cl upload will prefill the description
1505 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001506 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1507 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001508
1509 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001510 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001511 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001512 name,
1513 description,
1514 absroot,
1515 files,
1516 issue,
1517 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001518 author,
1519 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001520
dsansomee2d6fd92016-09-08 00:10:47 -07001521 def UpdateDescription(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001522 self.description = description
dsansomee2d6fd92016-09-08 00:10:47 -07001523 return self._codereview_impl.UpdateDescriptionRemote(
1524 description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001525
1526 def RunHook(self, committing, may_prompt, verbose, change):
1527 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1528 try:
1529 return presubmit_support.DoPresubmitChecks(change, committing,
1530 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1531 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001532 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1533 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001534 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001535 DieWithError(
1536 ('%s\nMaybe your depot_tools is out of date?\n'
1537 'If all fails, contact maruel@') % e)
1538
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001539 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1540 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001541 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1542 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001543 else:
1544 # Assume url.
1545 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1546 urlparse.urlparse(issue_arg))
1547 if not parsed_issue_arg or not parsed_issue_arg.valid:
1548 DieWithError('Failed to parse issue argument "%s". '
1549 'Must be an issue number or a valid URL.' % issue_arg)
1550 return self._codereview_impl.CMDPatchWithParsedIssue(
1551 parsed_issue_arg, reject, nocommit, directory)
1552
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001553 def CMDUpload(self, options, git_diff_args, orig_args):
1554 """Uploads a change to codereview."""
1555 if git_diff_args:
1556 # TODO(ukai): is it ok for gerrit case?
1557 base_branch = git_diff_args[0]
1558 else:
1559 if self.GetBranch() is None:
1560 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1561
1562 # Default to diffing against common ancestor of upstream branch
1563 base_branch = self.GetCommonAncestorWithUpstream()
1564 git_diff_args = [base_branch, 'HEAD']
1565
1566 # Make sure authenticated to codereview before running potentially expensive
1567 # hooks. It is a fast, best efforts check. Codereview still can reject the
1568 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001569 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001570
1571 # Apply watchlists on upload.
1572 change = self.GetChange(base_branch, None)
1573 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1574 files = [f.LocalPath() for f in change.AffectedFiles()]
1575 if not options.bypass_watchlists:
1576 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1577
1578 if not options.bypass_hooks:
1579 if options.reviewers or options.tbr_owners:
1580 # Set the reviewer list now so that presubmit checks can access it.
1581 change_description = ChangeDescription(change.FullDescriptionText())
1582 change_description.update_reviewers(options.reviewers,
1583 options.tbr_owners,
1584 change)
1585 change.SetDescriptionText(change_description.description)
1586 hook_results = self.RunHook(committing=False,
1587 may_prompt=not options.force,
1588 verbose=options.verbose,
1589 change=change)
1590 if not hook_results.should_continue():
1591 return 1
1592 if not options.reviewers and hook_results.reviewers:
1593 options.reviewers = hook_results.reviewers.split(',')
1594
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001595 # TODO(tandrii): Checking local patchset against remote patchset is only
1596 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1597 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001598 latest_patchset = self.GetMostRecentPatchset()
1599 local_patchset = self.GetPatchset()
1600 if (latest_patchset and local_patchset and
1601 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001602 print('The last upload made from this repository was patchset #%d but '
1603 'the most recent patchset on the server is #%d.'
1604 % (local_patchset, latest_patchset))
1605 print('Uploading will still work, but if you\'ve uploaded to this '
1606 'issue from another machine or branch the patch you\'re '
1607 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001608 ask_for_data('About to upload; enter to confirm.')
1609
1610 print_stats(options.similarity, options.find_copies, git_diff_args)
1611 ret = self.CMDUploadChange(options, git_diff_args, change)
1612 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001613 if options.use_commit_queue:
1614 self.SetCQState(_CQState.COMMIT)
1615 elif options.cq_dry_run:
1616 self.SetCQState(_CQState.DRY_RUN)
1617
tandrii5d48c322016-08-18 16:19:37 -07001618 _git_set_branch_config_value('last-upload-hash',
1619 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001620 # Run post upload hooks, if specified.
1621 if settings.GetRunPostUploadHook():
1622 presubmit_support.DoPostUploadExecuter(
1623 change,
1624 self,
1625 settings.GetRoot(),
1626 options.verbose,
1627 sys.stdout)
1628
1629 # Upload all dependencies if specified.
1630 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001631 print()
1632 print('--dependencies has been specified.')
1633 print('All dependent local branches will be re-uploaded.')
1634 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001635 # Remove the dependencies flag from args so that we do not end up in a
1636 # loop.
1637 orig_args.remove('--dependencies')
1638 ret = upload_branch_deps(self, orig_args)
1639 return ret
1640
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001641 def SetCQState(self, new_state):
1642 """Update the CQ state for latest patchset.
1643
1644 Issue must have been already uploaded and known.
1645 """
1646 assert new_state in _CQState.ALL_STATES
1647 assert self.GetIssue()
1648 return self._codereview_impl.SetCQState(new_state)
1649
qyearsley1fdfcb62016-10-24 13:22:03 -07001650 def TriggerDryRun(self):
1651 """Triggers a dry run and prints a warning on failure."""
1652 # TODO(qyearsley): Either re-use this method in CMDset_commit
1653 # and CMDupload, or change CMDtry to trigger dry runs with
1654 # just SetCQState, and catch keyboard interrupt and other
1655 # errors in that method.
1656 try:
1657 self.SetCQState(_CQState.DRY_RUN)
1658 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1659 return 0
1660 except KeyboardInterrupt:
1661 raise
1662 except:
1663 print('WARNING: failed to trigger CQ Dry Run.\n'
1664 'Either:\n'
1665 ' * your project has no CQ\n'
1666 ' * you don\'t have permission to trigger Dry Run\n'
1667 ' * bug in this code (see stack trace below).\n'
1668 'Consider specifying which bots to trigger manually '
1669 'or asking your project owners for permissions '
1670 'or contacting Chrome Infrastructure team at '
1671 'https://www.chromium.org/infra\n\n')
1672 # Still raise exception so that stack trace is printed.
1673 raise
1674
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001675 # Forward methods to codereview specific implementation.
1676
1677 def CloseIssue(self):
1678 return self._codereview_impl.CloseIssue()
1679
1680 def GetStatus(self):
1681 return self._codereview_impl.GetStatus()
1682
1683 def GetCodereviewServer(self):
1684 return self._codereview_impl.GetCodereviewServer()
1685
tandriide281ae2016-10-12 06:02:30 -07001686 def GetIssueOwner(self):
1687 """Get owner from codereview, which may differ from this checkout."""
1688 return self._codereview_impl.GetIssueOwner()
1689
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001690 def GetApprovingReviewers(self):
1691 return self._codereview_impl.GetApprovingReviewers()
1692
1693 def GetMostRecentPatchset(self):
1694 return self._codereview_impl.GetMostRecentPatchset()
1695
tandriide281ae2016-10-12 06:02:30 -07001696 def CannotTriggerTryJobReason(self):
1697 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1698 return self._codereview_impl.CannotTriggerTryJobReason()
1699
tandrii8c5a3532016-11-04 07:52:02 -07001700 def GetTryjobProperties(self, patchset=None):
1701 """Returns dictionary of properties to launch tryjob."""
1702 return self._codereview_impl.GetTryjobProperties(patchset=patchset)
1703
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001704 def __getattr__(self, attr):
1705 # This is because lots of untested code accesses Rietveld-specific stuff
1706 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001707 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001708 # Note that child method defines __getattr__ as well, and forwards it here,
1709 # because _RietveldChangelistImpl is not cleaned up yet, and given
1710 # deprecation of Rietveld, it should probably be just removed.
1711 # Until that time, avoid infinite recursion by bypassing __getattr__
1712 # of implementation class.
1713 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001714
1715
1716class _ChangelistCodereviewBase(object):
1717 """Abstract base class encapsulating codereview specifics of a changelist."""
1718 def __init__(self, changelist):
1719 self._changelist = changelist # instance of Changelist
1720
1721 def __getattr__(self, attr):
1722 # Forward methods to changelist.
1723 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1724 # _RietveldChangelistImpl to avoid this hack?
1725 return getattr(self._changelist, attr)
1726
1727 def GetStatus(self):
1728 """Apply a rough heuristic to give a simple summary of an issue's review
1729 or CQ status, assuming adherence to a common workflow.
1730
1731 Returns None if no issue for this branch, or specific string keywords.
1732 """
1733 raise NotImplementedError()
1734
1735 def GetCodereviewServer(self):
1736 """Returns server URL without end slash, like "https://codereview.com"."""
1737 raise NotImplementedError()
1738
1739 def FetchDescription(self):
1740 """Fetches and returns description from the codereview server."""
1741 raise NotImplementedError()
1742
tandrii5d48c322016-08-18 16:19:37 -07001743 @classmethod
1744 def IssueConfigKey(cls):
1745 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001746 raise NotImplementedError()
1747
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001748 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001749 def PatchsetConfigKey(cls):
1750 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001751 raise NotImplementedError()
1752
tandrii5d48c322016-08-18 16:19:37 -07001753 @classmethod
1754 def CodereviewServerConfigKey(cls):
1755 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001756 raise NotImplementedError()
1757
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001758 def _PostUnsetIssueProperties(self):
1759 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001760 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001761
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001762 def GetRieveldObjForPresubmit(self):
1763 # This is an unfortunate Rietveld-embeddedness in presubmit.
1764 # For non-Rietveld codereviews, this probably should return a dummy object.
1765 raise NotImplementedError()
1766
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001767 def GetGerritObjForPresubmit(self):
1768 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1769 return None
1770
dsansomee2d6fd92016-09-08 00:10:47 -07001771 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001772 """Update the description on codereview site."""
1773 raise NotImplementedError()
1774
1775 def CloseIssue(self):
1776 """Closes the issue."""
1777 raise NotImplementedError()
1778
1779 def GetApprovingReviewers(self):
1780 """Returns a list of reviewers approving the change.
1781
1782 Note: not necessarily committers.
1783 """
1784 raise NotImplementedError()
1785
1786 def GetMostRecentPatchset(self):
1787 """Returns the most recent patchset number from the codereview site."""
1788 raise NotImplementedError()
1789
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001790 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1791 directory):
1792 """Fetches and applies the issue.
1793
1794 Arguments:
1795 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1796 reject: if True, reject the failed patch instead of switching to 3-way
1797 merge. Rietveld only.
1798 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1799 only.
1800 directory: switch to directory before applying the patch. Rietveld only.
1801 """
1802 raise NotImplementedError()
1803
1804 @staticmethod
1805 def ParseIssueURL(parsed_url):
1806 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1807 failed."""
1808 raise NotImplementedError()
1809
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001810 def EnsureAuthenticated(self, force):
1811 """Best effort check that user is authenticated with codereview server.
1812
1813 Arguments:
1814 force: whether to skip confirmation questions.
1815 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001816 raise NotImplementedError()
1817
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001818 def CMDUploadChange(self, options, args, change):
1819 """Uploads a change to codereview."""
1820 raise NotImplementedError()
1821
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001822 def SetCQState(self, new_state):
1823 """Update the CQ state for latest patchset.
1824
1825 Issue must have been already uploaded and known.
1826 """
1827 raise NotImplementedError()
1828
tandriie113dfd2016-10-11 10:20:12 -07001829 def CannotTriggerTryJobReason(self):
1830 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1831 raise NotImplementedError()
1832
tandriide281ae2016-10-12 06:02:30 -07001833 def GetIssueOwner(self):
1834 raise NotImplementedError()
1835
tandrii8c5a3532016-11-04 07:52:02 -07001836 def GetTryjobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001837 raise NotImplementedError()
1838
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001839
1840class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1841 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1842 super(_RietveldChangelistImpl, self).__init__(changelist)
1843 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001844 if not rietveld_server:
1845 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001846
1847 self._rietveld_server = rietveld_server
1848 self._auth_config = auth_config
1849 self._props = None
1850 self._rpc_server = None
1851
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001852 def GetCodereviewServer(self):
1853 if not self._rietveld_server:
1854 # If we're on a branch then get the server potentially associated
1855 # with that branch.
1856 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001857 self._rietveld_server = gclient_utils.UpgradeToHttps(
1858 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001859 if not self._rietveld_server:
1860 self._rietveld_server = settings.GetDefaultServerUrl()
1861 return self._rietveld_server
1862
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001863 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001864 """Best effort check that user is authenticated with Rietveld server."""
1865 if self._auth_config.use_oauth2:
1866 authenticator = auth.get_authenticator_for_host(
1867 self.GetCodereviewServer(), self._auth_config)
1868 if not authenticator.has_cached_credentials():
1869 raise auth.LoginRequiredError(self.GetCodereviewServer())
1870
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001871 def FetchDescription(self):
1872 issue = self.GetIssue()
1873 assert issue
1874 try:
1875 return self.RpcServer().get_description(issue).strip()
1876 except urllib2.HTTPError as e:
1877 if e.code == 404:
1878 DieWithError(
1879 ('\nWhile fetching the description for issue %d, received a '
1880 '404 (not found)\n'
1881 'error. It is likely that you deleted this '
1882 'issue on the server. If this is the\n'
1883 'case, please run\n\n'
1884 ' git cl issue 0\n\n'
1885 'to clear the association with the deleted issue. Then run '
1886 'this command again.') % issue)
1887 else:
1888 DieWithError(
1889 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1890 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001891 print('Warning: Failed to retrieve CL description due to network '
1892 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001893 return ''
1894
1895 def GetMostRecentPatchset(self):
1896 return self.GetIssueProperties()['patchsets'][-1]
1897
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001898 def GetIssueProperties(self):
1899 if self._props is None:
1900 issue = self.GetIssue()
1901 if not issue:
1902 self._props = {}
1903 else:
1904 self._props = self.RpcServer().get_issue_properties(issue, True)
1905 return self._props
1906
tandriie113dfd2016-10-11 10:20:12 -07001907 def CannotTriggerTryJobReason(self):
1908 props = self.GetIssueProperties()
1909 if not props:
1910 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1911 if props.get('closed'):
1912 return 'CL %s is closed' % self.GetIssue()
1913 if props.get('private'):
1914 return 'CL %s is private' % self.GetIssue()
1915 return None
1916
tandrii8c5a3532016-11-04 07:52:02 -07001917 def GetTryjobProperties(self, patchset=None):
1918 """Returns dictionary of properties to launch tryjob."""
1919 project = (self.GetIssueProperties() or {}).get('project')
1920 return {
1921 'issue': self.GetIssue(),
1922 'patch_project': project,
1923 'patch_storage': 'rietveld',
1924 'patchset': patchset or self.GetPatchset(),
1925 'rietveld': self.GetCodereviewServer(),
1926 }
1927
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001928 def GetApprovingReviewers(self):
1929 return get_approving_reviewers(self.GetIssueProperties())
1930
tandriide281ae2016-10-12 06:02:30 -07001931 def GetIssueOwner(self):
1932 return (self.GetIssueProperties() or {}).get('owner_email')
1933
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001934 def AddComment(self, message):
1935 return self.RpcServer().add_comment(self.GetIssue(), message)
1936
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001937 def GetStatus(self):
1938 """Apply a rough heuristic to give a simple summary of an issue's review
1939 or CQ status, assuming adherence to a common workflow.
1940
1941 Returns None if no issue for this branch, or one of the following keywords:
1942 * 'error' - error from review tool (including deleted issues)
1943 * 'unsent' - not sent for review
1944 * 'waiting' - waiting for review
1945 * 'reply' - waiting for owner to reply to review
1946 * 'lgtm' - LGTM from at least one approved reviewer
1947 * 'commit' - in the commit queue
1948 * 'closed' - closed
1949 """
1950 if not self.GetIssue():
1951 return None
1952
1953 try:
1954 props = self.GetIssueProperties()
1955 except urllib2.HTTPError:
1956 return 'error'
1957
1958 if props.get('closed'):
1959 # Issue is closed.
1960 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001961 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001962 # Issue is in the commit queue.
1963 return 'commit'
1964
1965 try:
1966 reviewers = self.GetApprovingReviewers()
1967 except urllib2.HTTPError:
1968 return 'error'
1969
1970 if reviewers:
1971 # Was LGTM'ed.
1972 return 'lgtm'
1973
1974 messages = props.get('messages') or []
1975
tandrii9d2c7a32016-06-22 03:42:45 -07001976 # Skip CQ messages that don't require owner's action.
1977 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1978 if 'Dry run:' in messages[-1]['text']:
1979 messages.pop()
1980 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1981 # This message always follows prior messages from CQ,
1982 # so skip this too.
1983 messages.pop()
1984 else:
1985 # This is probably a CQ messages warranting user attention.
1986 break
1987
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001988 if not messages:
1989 # No message was sent.
1990 return 'unsent'
1991 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001992 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001993 return 'reply'
1994 return 'waiting'
1995
dsansomee2d6fd92016-09-08 00:10:47 -07001996 def UpdateDescriptionRemote(self, description, force=False):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001997 return self.RpcServer().update_description(
1998 self.GetIssue(), self.description)
1999
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002000 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002001 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002002
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002003 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002004 return self.SetFlags({flag: value})
2005
2006 def SetFlags(self, flags):
2007 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002008 """
phajdan.jr68598232016-08-10 03:28:28 -07002009 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002010 try:
tandrii4b233bd2016-07-06 03:50:29 -07002011 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002012 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002013 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002014 if e.code == 404:
2015 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2016 if e.code == 403:
2017 DieWithError(
2018 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002019 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002020 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002021
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002022 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002023 """Returns an upload.RpcServer() to access this review's rietveld instance.
2024 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002025 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002026 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002027 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002028 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002029 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002030
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002031 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002032 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002033 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002034
tandrii5d48c322016-08-18 16:19:37 -07002035 @classmethod
2036 def PatchsetConfigKey(cls):
2037 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002038
tandrii5d48c322016-08-18 16:19:37 -07002039 @classmethod
2040 def CodereviewServerConfigKey(cls):
2041 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002042
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002043 def GetRieveldObjForPresubmit(self):
2044 return self.RpcServer()
2045
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002046 def SetCQState(self, new_state):
2047 props = self.GetIssueProperties()
2048 if props.get('private'):
2049 DieWithError('Cannot set-commit on private issue')
2050
2051 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002052 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002053 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002054 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002055 else:
tandrii4b233bd2016-07-06 03:50:29 -07002056 assert new_state == _CQState.DRY_RUN
2057 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002058
2059
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002060 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2061 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002062 # PatchIssue should never be called with a dirty tree. It is up to the
2063 # caller to check this, but just in case we assert here since the
2064 # consequences of the caller not checking this could be dire.
2065 assert(not git_common.is_dirty_git_tree('apply'))
2066 assert(parsed_issue_arg.valid)
2067 self._changelist.issue = parsed_issue_arg.issue
2068 if parsed_issue_arg.hostname:
2069 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2070
skobes6468b902016-10-24 08:45:10 -07002071 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2072 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2073 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002074 try:
skobes6468b902016-10-24 08:45:10 -07002075 scm_obj.apply_patch(patchset_object)
2076 except Exception as e:
2077 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002078 return 1
2079
2080 # If we had an issue, commit the current state and register the issue.
2081 if not nocommit:
2082 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2083 'patch from issue %(i)s at patchset '
2084 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2085 % {'i': self.GetIssue(), 'p': patchset})])
2086 self.SetIssue(self.GetIssue())
2087 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07002088 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002089 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002090 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002091 return 0
2092
2093 @staticmethod
2094 def ParseIssueURL(parsed_url):
2095 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2096 return None
wychen3c1c1722016-08-04 11:46:36 -07002097 # Rietveld patch: https://domain/<number>/#ps<patchset>
2098 match = re.match(r'/(\d+)/$', parsed_url.path)
2099 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2100 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002101 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002102 issue=int(match.group(1)),
2103 patchset=int(match2.group(1)),
2104 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002105 # Typical url: https://domain/<issue_number>[/[other]]
2106 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2107 if match:
skobes6468b902016-10-24 08:45:10 -07002108 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002109 issue=int(match.group(1)),
2110 hostname=parsed_url.netloc)
2111 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2112 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2113 if match:
skobes6468b902016-10-24 08:45:10 -07002114 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002115 issue=int(match.group(1)),
2116 patchset=int(match.group(2)),
skobes6468b902016-10-24 08:45:10 -07002117 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002118 return None
2119
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002120 def CMDUploadChange(self, options, args, change):
2121 """Upload the patch to Rietveld."""
2122 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2123 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002124 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2125 if options.emulate_svn_auto_props:
2126 upload_args.append('--emulate_svn_auto_props')
2127
2128 change_desc = None
2129
2130 if options.email is not None:
2131 upload_args.extend(['--email', options.email])
2132
2133 if self.GetIssue():
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])
2136 if options.message:
2137 upload_args.extend(['--message', options.message])
2138 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002139 print('This branch is associated with issue %s. '
2140 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002141 else:
nodirca166002016-06-27 10:59:51 -07002142 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002143 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002144 if options.message:
2145 message = options.message
2146 else:
2147 message = CreateDescriptionFromLog(args)
2148 if options.title:
2149 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002150 change_desc = ChangeDescription(message)
2151 if options.reviewers or options.tbr_owners:
2152 change_desc.update_reviewers(options.reviewers,
2153 options.tbr_owners,
2154 change)
2155 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002156 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002157
2158 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002159 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002160 return 1
2161
2162 upload_args.extend(['--message', change_desc.description])
2163 if change_desc.get_reviewers():
2164 upload_args.append('--reviewers=%s' % ','.join(
2165 change_desc.get_reviewers()))
2166 if options.send_mail:
2167 if not change_desc.get_reviewers():
2168 DieWithError("Must specify reviewers to send email.")
2169 upload_args.append('--send_mail')
2170
2171 # We check this before applying rietveld.private assuming that in
2172 # rietveld.cc only addresses which we can send private CLs to are listed
2173 # if rietveld.private is set, and so we should ignore rietveld.cc only
2174 # when --private is specified explicitly on the command line.
2175 if options.private:
2176 logging.warn('rietveld.cc is ignored since private flag is specified. '
2177 'You need to review and add them manually if necessary.')
2178 cc = self.GetCCListWithoutDefault()
2179 else:
2180 cc = self.GetCCList()
2181 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002182 if change_desc.get_cced():
2183 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002184 if cc:
2185 upload_args.extend(['--cc', cc])
2186
2187 if options.private or settings.GetDefaultPrivateFlag() == "True":
2188 upload_args.append('--private')
2189
2190 upload_args.extend(['--git_similarity', str(options.similarity)])
2191 if not options.find_copies:
2192 upload_args.extend(['--git_no_find_copies'])
2193
2194 # Include the upstream repo's URL in the change -- this is useful for
2195 # projects that have their source spread across multiple repos.
2196 remote_url = self.GetGitBaseUrlFromConfig()
2197 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002198 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2199 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2200 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002201 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002202 remote, remote_branch = self.GetRemoteBranch()
2203 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01002204 pending_prefix_check=True,
2205 remote_url=self.GetRemoteUrl())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002206 if target_ref:
2207 upload_args.extend(['--target_ref', target_ref])
2208
2209 # Look for dependent patchsets. See crbug.com/480453 for more details.
2210 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2211 upstream_branch = ShortBranchName(upstream_branch)
2212 if remote is '.':
2213 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002214 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002215 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002216 print()
2217 print('Skipping dependency patchset upload because git config '
2218 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2219 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002220 else:
2221 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002222 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002223 auth_config=auth_config)
2224 branch_cl_issue_url = branch_cl.GetIssueURL()
2225 branch_cl_issue = branch_cl.GetIssue()
2226 branch_cl_patchset = branch_cl.GetPatchset()
2227 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2228 upload_args.extend(
2229 ['--depends_on_patchset', '%s:%s' % (
2230 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002231 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002232 '\n'
2233 'The current branch (%s) is tracking a local branch (%s) with '
2234 'an associated CL.\n'
2235 'Adding %s/#ps%s as a dependency patchset.\n'
2236 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2237 branch_cl_patchset))
2238
2239 project = settings.GetProject()
2240 if project:
2241 upload_args.extend(['--project', project])
2242
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002243 try:
2244 upload_args = ['upload'] + upload_args + args
2245 logging.info('upload.RealMain(%s)', upload_args)
2246 issue, patchset = upload.RealMain(upload_args)
2247 issue = int(issue)
2248 patchset = int(patchset)
2249 except KeyboardInterrupt:
2250 sys.exit(1)
2251 except:
2252 # If we got an exception after the user typed a description for their
2253 # change, back up the description before re-raising.
2254 if change_desc:
2255 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2256 print('\nGot exception while uploading -- saving description to %s\n' %
2257 backup_path)
2258 backup_file = open(backup_path, 'w')
2259 backup_file.write(change_desc.description)
2260 backup_file.close()
2261 raise
2262
2263 if not self.GetIssue():
2264 self.SetIssue(issue)
2265 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002266 return 0
2267
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002268
2269class _GerritChangelistImpl(_ChangelistCodereviewBase):
2270 def __init__(self, changelist, auth_config=None):
2271 # auth_config is Rietveld thing, kept here to preserve interface only.
2272 super(_GerritChangelistImpl, self).__init__(changelist)
2273 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002274 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002275 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002276 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002277
2278 def _GetGerritHost(self):
2279 # Lazy load of configs.
2280 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002281 if self._gerrit_host and '.' not in self._gerrit_host:
2282 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2283 # This happens for internal stuff http://crbug.com/614312.
2284 parsed = urlparse.urlparse(self.GetRemoteUrl())
2285 if parsed.scheme == 'sso':
2286 print('WARNING: using non https URLs for remote is likely broken\n'
2287 ' Your current remote is: %s' % self.GetRemoteUrl())
2288 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2289 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002290 return self._gerrit_host
2291
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002292 def _GetGitHost(self):
2293 """Returns git host to be used when uploading change to Gerrit."""
2294 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2295
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002296 def GetCodereviewServer(self):
2297 if not self._gerrit_server:
2298 # If we're on a branch then get the server potentially associated
2299 # with that branch.
2300 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002301 self._gerrit_server = self._GitGetBranchConfigValue(
2302 self.CodereviewServerConfigKey())
2303 if self._gerrit_server:
2304 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002305 if not self._gerrit_server:
2306 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2307 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002308 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002309 parts[0] = parts[0] + '-review'
2310 self._gerrit_host = '.'.join(parts)
2311 self._gerrit_server = 'https://%s' % self._gerrit_host
2312 return self._gerrit_server
2313
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002314 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002315 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002316 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002317
tandrii5d48c322016-08-18 16:19:37 -07002318 @classmethod
2319 def PatchsetConfigKey(cls):
2320 return 'gerritpatchset'
2321
2322 @classmethod
2323 def CodereviewServerConfigKey(cls):
2324 return 'gerritserver'
2325
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002326 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002327 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002328 if settings.GetGerritSkipEnsureAuthenticated():
2329 # For projects with unusual authentication schemes.
2330 # See http://crbug.com/603378.
2331 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002332 # Lazy-loader to identify Gerrit and Git hosts.
2333 if gerrit_util.GceAuthenticator.is_gce():
2334 return
2335 self.GetCodereviewServer()
2336 git_host = self._GetGitHost()
2337 assert self._gerrit_server and self._gerrit_host
2338 cookie_auth = gerrit_util.CookiesAuthenticator()
2339
2340 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2341 git_auth = cookie_auth.get_auth_header(git_host)
2342 if gerrit_auth and git_auth:
2343 if gerrit_auth == git_auth:
2344 return
2345 print((
2346 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2347 ' Check your %s or %s file for credentials of hosts:\n'
2348 ' %s\n'
2349 ' %s\n'
2350 ' %s') %
2351 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2352 git_host, self._gerrit_host,
2353 cookie_auth.get_new_password_message(git_host)))
2354 if not force:
2355 ask_for_data('If you know what you are doing, press Enter to continue, '
2356 'Ctrl+C to abort.')
2357 return
2358 else:
2359 missing = (
2360 [] if gerrit_auth else [self._gerrit_host] +
2361 [] if git_auth else [git_host])
2362 DieWithError('Credentials for the following hosts are required:\n'
2363 ' %s\n'
2364 'These are read from %s (or legacy %s)\n'
2365 '%s' % (
2366 '\n '.join(missing),
2367 cookie_auth.get_gitcookies_path(),
2368 cookie_auth.get_netrc_path(),
2369 cookie_auth.get_new_password_message(git_host)))
2370
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002371 def _PostUnsetIssueProperties(self):
2372 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002373 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002374
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002375 def GetRieveldObjForPresubmit(self):
2376 class ThisIsNotRietveldIssue(object):
2377 def __nonzero__(self):
2378 # This is a hack to make presubmit_support think that rietveld is not
2379 # defined, yet still ensure that calls directly result in a decent
2380 # exception message below.
2381 return False
2382
2383 def __getattr__(self, attr):
2384 print(
2385 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2386 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2387 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2388 'or use Rietveld for codereview.\n'
2389 'See also http://crbug.com/579160.' % attr)
2390 raise NotImplementedError()
2391 return ThisIsNotRietveldIssue()
2392
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002393 def GetGerritObjForPresubmit(self):
2394 return presubmit_support.GerritAccessor(self._GetGerritHost())
2395
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002396 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002397 """Apply a rough heuristic to give a simple summary of an issue's review
2398 or CQ status, assuming adherence to a common workflow.
2399
2400 Returns None if no issue for this branch, or one of the following keywords:
2401 * 'error' - error from review tool (including deleted issues)
2402 * 'unsent' - no reviewers added
2403 * 'waiting' - waiting for review
2404 * 'reply' - waiting for owner to reply to review
Quinten Yearsley442fb642016-12-15 15:38:27 -08002405 * 'not lgtm' - Code-Review disapproval from at least one valid reviewer
tandriic2405f52016-10-10 08:13:15 -07002406 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002407 * 'commit' - in the commit queue
2408 * 'closed' - abandoned
2409 """
2410 if not self.GetIssue():
2411 return None
2412
2413 try:
2414 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002415 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002416 return 'error'
2417
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002418 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002419 return 'closed'
2420
2421 cq_label = data['labels'].get('Commit-Queue', {})
2422 if cq_label:
rmistryc9ebbd22016-10-14 12:35:54 -07002423 votes = cq_label.get('all', [])
2424 highest_vote = 0
2425 for v in votes:
2426 highest_vote = max(highest_vote, v.get('value', 0))
2427 vote_value = str(highest_vote)
2428 if vote_value != '0':
2429 # Add a '+' if the value is not 0 to match the values in the label.
2430 # The cq_label does not have negatives.
2431 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002432 vote_text = cq_label.get('values', {}).get(vote_value, '')
2433 if vote_text.lower() == 'commit':
2434 return 'commit'
2435
2436 lgtm_label = data['labels'].get('Code-Review', {})
2437 if lgtm_label:
2438 if 'rejected' in lgtm_label:
2439 return 'not lgtm'
2440 if 'approved' in lgtm_label:
2441 return 'lgtm'
2442
2443 if not data.get('reviewers', {}).get('REVIEWER', []):
2444 return 'unsent'
2445
2446 messages = data.get('messages', [])
2447 if messages:
2448 owner = data['owner'].get('_account_id')
2449 last_message_author = messages[-1].get('author', {}).get('_account_id')
2450 if owner != last_message_author:
2451 # Some reply from non-owner.
2452 return 'reply'
2453
2454 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002455
2456 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002457 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002458 return data['revisions'][data['current_revision']]['_number']
2459
2460 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002461 data = self._GetChangeDetail(['CURRENT_REVISION'])
2462 current_rev = data['current_revision']
2463 url = data['revisions'][current_rev]['fetch']['http']['url']
2464 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002465
dsansomee2d6fd92016-09-08 00:10:47 -07002466 def UpdateDescriptionRemote(self, description, force=False):
2467 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2468 if not force:
2469 ask_for_data(
2470 'The description cannot be modified while the issue has a pending '
2471 'unpublished edit. Either publish the edit in the Gerrit web UI '
2472 'or delete it.\n\n'
2473 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2474
2475 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2476 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002477 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002478 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002479
2480 def CloseIssue(self):
2481 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2482
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002483 def GetApprovingReviewers(self):
2484 """Returns a list of reviewers approving the change.
2485
2486 Note: not necessarily committers.
2487 """
2488 raise NotImplementedError()
2489
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002490 def SubmitIssue(self, wait_for_merge=True):
2491 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2492 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002493
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002494 def _GetChangeDetail(self, options=None, issue=None):
2495 options = options or []
2496 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002497 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002498 try:
2499 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2500 options, ignore_404=False)
2501 except gerrit_util.GerritError as e:
2502 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002503 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002504 raise
tandriic2405f52016-10-10 08:13:15 -07002505 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002506
agable32978d92016-11-01 12:55:02 -07002507 def _GetChangeCommit(self, issue=None):
2508 issue = issue or self.GetIssue()
2509 assert issue, 'issue is required to query Gerrit'
2510 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2511 if not data:
Aaron Gablea45ee112016-11-22 15:14:38 -08002512 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
agable32978d92016-11-01 12:55:02 -07002513 return data
2514
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002515 def CMDLand(self, force, bypass_hooks, verbose):
2516 if git_common.is_dirty_git_tree('land'):
2517 return 1
tandriid60367b2016-06-22 05:25:12 -07002518 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2519 if u'Commit-Queue' in detail.get('labels', {}):
2520 if not force:
2521 ask_for_data('\nIt seems this repository has a Commit Queue, '
2522 'which can test and land changes for you. '
2523 'Are you sure you wish to bypass it?\n'
2524 'Press Enter to continue, Ctrl+C to abort.')
2525
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002526 differs = True
tandriic4344b52016-08-29 06:04:54 -07002527 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002528 # Note: git diff outputs nothing if there is no diff.
2529 if not last_upload or RunGit(['diff', last_upload]).strip():
2530 print('WARNING: some changes from local branch haven\'t been uploaded')
2531 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002532 if detail['current_revision'] == last_upload:
2533 differs = False
2534 else:
2535 print('WARNING: local branch contents differ from latest uploaded '
2536 'patchset')
2537 if differs:
2538 if not force:
2539 ask_for_data(
Andrii Shyshkalov3aac7752016-11-16 15:23:13 +01002540 'Do you want to submit latest Gerrit patchset and bypass hooks?\n'
2541 'Press Enter to continue, Ctrl+C to abort.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002542 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2543 elif not bypass_hooks:
2544 hook_results = self.RunHook(
2545 committing=True,
2546 may_prompt=not force,
2547 verbose=verbose,
2548 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2549 if not hook_results.should_continue():
2550 return 1
2551
2552 self.SubmitIssue(wait_for_merge=True)
2553 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002554 links = self._GetChangeCommit().get('web_links', [])
2555 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002556 if link.get('name') == 'gitiles' and link.get('url'):
agable32978d92016-11-01 12:55:02 -07002557 print('Landed as %s' % link.get('url'))
2558 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002559 return 0
2560
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002561 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2562 directory):
2563 assert not reject
2564 assert not nocommit
2565 assert not directory
2566 assert parsed_issue_arg.valid
2567
2568 self._changelist.issue = parsed_issue_arg.issue
2569
2570 if parsed_issue_arg.hostname:
2571 self._gerrit_host = parsed_issue_arg.hostname
2572 self._gerrit_server = 'https://%s' % self._gerrit_host
2573
tandriic2405f52016-10-10 08:13:15 -07002574 try:
2575 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002576 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002577 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002578
2579 if not parsed_issue_arg.patchset:
2580 # Use current revision by default.
2581 revision_info = detail['revisions'][detail['current_revision']]
2582 patchset = int(revision_info['_number'])
2583 else:
2584 patchset = parsed_issue_arg.patchset
2585 for revision_info in detail['revisions'].itervalues():
2586 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2587 break
2588 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002589 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002590 (parsed_issue_arg.patchset, self.GetIssue()))
2591
2592 fetch_info = revision_info['fetch']['http']
2593 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2594 RunGit(['cherry-pick', 'FETCH_HEAD'])
2595 self.SetIssue(self.GetIssue())
2596 self.SetPatchset(patchset)
Aaron Gablea45ee112016-11-22 15:14:38 -08002597 print('Committed patch for change %i patchset %i locally' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002598 (self.GetIssue(), self.GetPatchset()))
2599 return 0
2600
2601 @staticmethod
2602 def ParseIssueURL(parsed_url):
2603 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2604 return None
2605 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2606 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2607 # Short urls like https://domain/<issue_number> can be used, but don't allow
2608 # specifying the patchset (you'd 404), but we allow that here.
2609 if parsed_url.path == '/':
2610 part = parsed_url.fragment
2611 else:
2612 part = parsed_url.path
2613 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2614 if match:
2615 return _ParsedIssueNumberArgument(
2616 issue=int(match.group(2)),
2617 patchset=int(match.group(4)) if match.group(4) else None,
2618 hostname=parsed_url.netloc)
2619 return None
2620
tandrii16e0b4e2016-06-07 10:34:28 -07002621 def _GerritCommitMsgHookCheck(self, offer_removal):
2622 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2623 if not os.path.exists(hook):
2624 return
2625 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2626 # custom developer made one.
2627 data = gclient_utils.FileRead(hook)
2628 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2629 return
2630 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002631 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002632 'and may interfere with it in subtle ways.\n'
2633 'We recommend you remove the commit-msg hook.')
2634 if offer_removal:
2635 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2636 if reply.lower().startswith('y'):
2637 gclient_utils.rm_file_or_tree(hook)
2638 print('Gerrit commit-msg hook removed.')
2639 else:
2640 print('OK, will keep Gerrit commit-msg hook in place.')
2641
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002642 def CMDUploadChange(self, options, args, change):
2643 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002644 if options.squash and options.no_squash:
2645 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002646
2647 if not options.squash and not options.no_squash:
2648 # Load default for user, repo, squash=true, in this order.
2649 options.squash = settings.GetSquashGerritUploads()
2650 elif options.no_squash:
2651 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002652
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002653 # We assume the remote called "origin" is the one we want.
2654 # It is probably not worthwhile to support different workflows.
2655 gerrit_remote = 'origin'
2656
2657 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01002658 # Gerrit will not support pending prefix at all.
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002659 branch = GetTargetRef(remote, remote_branch, options.target_branch,
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01002660 pending_prefix_check=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002661
Aaron Gableb56ad332017-01-06 15:24:31 -08002662 # This may be None; default fallback value is determined in logic below.
2663 title = options.title
2664
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002665 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002666 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002667 if self.GetIssue():
2668 # Try to get the message from a previous upload.
2669 message = self.GetDescription()
2670 if not message:
2671 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002672 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002673 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002674 if not title:
2675 default_title = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
2676 title = ask_for_data(
2677 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002678 change_id = self._GetChangeDetail()['change_id']
2679 while True:
2680 footer_change_ids = git_footers.get_footer_change_id(message)
2681 if footer_change_ids == [change_id]:
2682 break
2683 if not footer_change_ids:
2684 message = git_footers.add_footer_change_id(message, change_id)
Aaron Gablea45ee112016-11-22 15:14:38 -08002685 print('WARNING: appended missing Change-Id to change description')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002686 continue
2687 # There is already a valid footer but with different or several ids.
2688 # Doing this automatically is non-trivial as we don't want to lose
2689 # existing other footers, yet we want to append just 1 desired
2690 # Change-Id. Thus, just create a new footer, but let user verify the
2691 # new description.
2692 message = '%s\n\nChange-Id: %s' % (message, change_id)
2693 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002694 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002695 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002696 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002697 'Please, check the proposed correction to the description, '
2698 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2699 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2700 change_id))
2701 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2702 if not options.force:
2703 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002704 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002705 message = change_desc.description
2706 if not message:
2707 DieWithError("Description is empty. Aborting...")
2708 # Continue the while loop.
2709 # Sanity check of this code - we should end up with proper message
2710 # footer.
2711 assert [change_id] == git_footers.get_footer_change_id(message)
2712 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002713 else: # if not self.GetIssue()
2714 if options.message:
2715 message = options.message
2716 else:
2717 message = CreateDescriptionFromLog(args)
2718 if options.title:
2719 message = options.title + '\n\n' + message
2720 change_desc = ChangeDescription(message)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002721 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002722 change_desc.prompt(bug=options.bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002723 # On first upload, patchset title is always this string, while
2724 # --title flag gets converted to first line of message.
2725 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002726 if not change_desc.description:
2727 DieWithError("Description is empty. Aborting...")
2728 message = change_desc.description
2729 change_ids = git_footers.get_footer_change_id(message)
2730 if len(change_ids) > 1:
2731 DieWithError('too many Change-Id footers, at most 1 allowed.')
2732 if not change_ids:
2733 # Generate the Change-Id automatically.
2734 message = git_footers.add_footer_change_id(
2735 message, GenerateGerritChangeId(message))
2736 change_desc.set_description(message)
2737 change_ids = git_footers.get_footer_change_id(message)
2738 assert len(change_ids) == 1
2739 change_id = change_ids[0]
2740
2741 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2742 if remote is '.':
2743 # If our upstream branch is local, we base our squashed commit on its
2744 # squashed version.
2745 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2746 # Check the squashed hash of the parent.
2747 parent = RunGit(['config',
2748 'branch.%s.gerritsquashhash' % upstream_branch_name],
2749 error_ok=True).strip()
2750 # Verify that the upstream branch has been uploaded too, otherwise
2751 # Gerrit will create additional CLs when uploading.
2752 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2753 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002754 DieWithError(
Aaron Gable48746652017-01-06 11:49:21 -08002755 '\nUpload upstream branch %s first.\n'
2756 'It is likely that this branch has been rebased since its last '
2757 'upload, so you just need to upload it again.\n'
2758 '(If you uploaded it with --no-squash, then branch dependencies '
2759 'are not supported, and you should reupload with --squash.)'
2760 % upstream_branch_name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002761 else:
2762 parent = self.GetCommonAncestorWithUpstream()
2763
2764 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2765 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2766 '-m', message]).strip()
2767 else:
2768 change_desc = ChangeDescription(
2769 options.message or CreateDescriptionFromLog(args))
2770 if not change_desc.description:
2771 DieWithError("Description is empty. Aborting...")
2772
2773 if not git_footers.get_footer_change_id(change_desc.description):
2774 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002775 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2776 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002777 ref_to_push = 'HEAD'
2778 parent = '%s/%s' % (gerrit_remote, branch)
2779 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2780
2781 assert change_desc
2782 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2783 ref_to_push)]).splitlines()
2784 if len(commits) > 1:
2785 print('WARNING: This will upload %d commits. Run the following command '
2786 'to see which commits will be uploaded: ' % len(commits))
2787 print('git log %s..%s' % (parent, ref_to_push))
2788 print('You can also use `git squash-branch` to squash these into a '
2789 'single commit.')
2790 ask_for_data('About to upload; enter to confirm.')
2791
2792 if options.reviewers or options.tbr_owners:
2793 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2794 change)
2795
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002796 # Extra options that can be specified at push time. Doc:
2797 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2798 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002799 if change_desc.get_reviewers(tbr_only=True):
2800 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2801 refspec_opts.append('l=Code-Review+1')
2802
Aaron Gable9b713dd2016-12-14 16:04:21 -08002803 if title:
2804 if not re.match(r'^[\w ]+$', title):
2805 title = re.sub(r'[^\w ]', '', title)
tandriieefe8322016-08-17 10:12:24 -07002806 print('WARNING: Patchset title may only contain alphanumeric chars '
Aaron Gable9b713dd2016-12-14 16:04:21 -08002807 'and spaces. Cleaned up title:\n%s' % title)
tandriieefe8322016-08-17 10:12:24 -07002808 if not options.force:
2809 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002810 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2811 # reverse on its side.
Aaron Gable9b713dd2016-12-14 16:04:21 -08002812 refspec_opts.append('m=' + title.replace(' ', '_'))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002813
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002814 if options.send_mail:
2815 if not change_desc.get_reviewers():
2816 DieWithError('Must specify reviewers to send email.')
2817 refspec_opts.append('notify=ALL')
2818 else:
2819 refspec_opts.append('notify=NONE')
2820
tandrii99a72f22016-08-17 14:33:24 -07002821 reviewers = change_desc.get_reviewers()
2822 if reviewers:
2823 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002824
agablec6787972016-09-09 16:13:34 -07002825 if options.private:
2826 refspec_opts.append('draft')
2827
rmistry9eadede2016-09-19 11:22:43 -07002828 if options.topic:
2829 # Documentation on Gerrit topics is here:
2830 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2831 refspec_opts.append('topic=%s' % options.topic)
2832
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002833 refspec_suffix = ''
2834 if refspec_opts:
2835 refspec_suffix = '%' + ','.join(refspec_opts)
2836 assert ' ' not in refspec_suffix, (
2837 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002838 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002839
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002840 try:
2841 push_stdout = gclient_utils.CheckCallAndFilter(
2842 ['git', 'push', gerrit_remote, refspec],
2843 print_stdout=True,
2844 # Flush after every line: useful for seeing progress when running as
2845 # recipe.
2846 filter_fn=lambda _: sys.stdout.flush())
2847 except subprocess2.CalledProcessError:
2848 DieWithError('Failed to create a change. Please examine output above '
2849 'for the reason of the failure. ')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002850
2851 if options.squash:
2852 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2853 change_numbers = [m.group(1)
2854 for m in map(regex.match, push_stdout.splitlines())
2855 if m]
2856 if len(change_numbers) != 1:
2857 DieWithError(
2858 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2859 'Change-Id: %s') % (len(change_numbers), change_id))
2860 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002861 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002862
2863 # Add cc's from the CC_LIST and --cc flag (if any).
2864 cc = self.GetCCList().split(',')
2865 if options.cc:
2866 cc.extend(options.cc)
2867 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002868 if change_desc.get_cced():
2869 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002870 if cc:
Aaron Gable5db82092016-11-17 10:52:08 -08002871 gerrit_util.AddReviewers(
Aaron Gable59f48512017-01-12 10:54:46 -08002872 self._GetGerritHost(), self.GetIssue(), cc,
2873 is_reviewer=False, notify=bool(options.send_mail))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002874 return 0
2875
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002876 def _AddChangeIdToCommitMessage(self, options, args):
2877 """Re-commits using the current message, assumes the commit hook is in
2878 place.
2879 """
2880 log_desc = options.message or CreateDescriptionFromLog(args)
2881 git_command = ['commit', '--amend', '-m', log_desc]
2882 RunGit(git_command)
2883 new_log_desc = CreateDescriptionFromLog(args)
2884 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002885 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002886 return new_log_desc
2887 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002888 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002889
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002890 def SetCQState(self, new_state):
2891 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002892 vote_map = {
2893 _CQState.NONE: 0,
2894 _CQState.DRY_RUN: 1,
2895 _CQState.COMMIT : 2,
2896 }
Andrii Shyshkalov828701b2016-12-09 10:46:47 +01002897 kwargs = {'labels': {'Commit-Queue': vote_map[new_state]}}
2898 if new_state == _CQState.DRY_RUN:
2899 # Don't spam everybody reviewer/owner.
2900 kwargs['notify'] = 'NONE'
2901 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(), **kwargs)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002902
tandriie113dfd2016-10-11 10:20:12 -07002903 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002904 try:
2905 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002906 except GerritChangeNotExists:
2907 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002908
2909 if data['status'] in ('ABANDONED', 'MERGED'):
2910 return 'CL %s is closed' % self.GetIssue()
2911
2912 def GetTryjobProperties(self, patchset=None):
2913 """Returns dictionary of properties to launch tryjob."""
2914 data = self._GetChangeDetail(['ALL_REVISIONS'])
2915 patchset = int(patchset or self.GetPatchset())
2916 assert patchset
2917 revision_data = None # Pylint wants it to be defined.
2918 for revision_data in data['revisions'].itervalues():
2919 if int(revision_data['_number']) == patchset:
2920 break
2921 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002922 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002923 (patchset, self.GetIssue()))
2924 return {
2925 'patch_issue': self.GetIssue(),
2926 'patch_set': patchset or self.GetPatchset(),
2927 'patch_project': data['project'],
2928 'patch_storage': 'gerrit',
2929 'patch_ref': revision_data['fetch']['http']['ref'],
2930 'patch_repository_url': revision_data['fetch']['http']['url'],
2931 'patch_gerrit_url': self.GetCodereviewServer(),
2932 }
tandriie113dfd2016-10-11 10:20:12 -07002933
tandriide281ae2016-10-12 06:02:30 -07002934 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002935 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002936
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002937
2938_CODEREVIEW_IMPLEMENTATIONS = {
2939 'rietveld': _RietveldChangelistImpl,
2940 'gerrit': _GerritChangelistImpl,
2941}
2942
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002943
iannuccie53c9352016-08-17 14:40:40 -07002944def _add_codereview_issue_select_options(parser, extra=""):
2945 _add_codereview_select_options(parser)
2946
2947 text = ('Operate on this issue number instead of the current branch\'s '
2948 'implicit issue.')
2949 if extra:
2950 text += ' '+extra
2951 parser.add_option('-i', '--issue', type=int, help=text)
2952
2953
2954def _process_codereview_issue_select_options(parser, options):
2955 _process_codereview_select_options(parser, options)
2956 if options.issue is not None and not options.forced_codereview:
2957 parser.error('--issue must be specified with either --rietveld or --gerrit')
2958
2959
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002960def _add_codereview_select_options(parser):
2961 """Appends --gerrit and --rietveld options to force specific codereview."""
2962 parser.codereview_group = optparse.OptionGroup(
2963 parser, 'EXPERIMENTAL! Codereview override options')
2964 parser.add_option_group(parser.codereview_group)
2965 parser.codereview_group.add_option(
2966 '--gerrit', action='store_true',
2967 help='Force the use of Gerrit for codereview')
2968 parser.codereview_group.add_option(
2969 '--rietveld', action='store_true',
2970 help='Force the use of Rietveld for codereview')
2971
2972
2973def _process_codereview_select_options(parser, options):
2974 if options.gerrit and options.rietveld:
2975 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2976 options.forced_codereview = None
2977 if options.gerrit:
2978 options.forced_codereview = 'gerrit'
2979 elif options.rietveld:
2980 options.forced_codereview = 'rietveld'
2981
2982
tandriif9aefb72016-07-01 09:06:51 -07002983def _get_bug_line_values(default_project, bugs):
2984 """Given default_project and comma separated list of bugs, yields bug line
2985 values.
2986
2987 Each bug can be either:
2988 * a number, which is combined with default_project
2989 * string, which is left as is.
2990
2991 This function may produce more than one line, because bugdroid expects one
2992 project per line.
2993
2994 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2995 ['v8:123', 'chromium:789']
2996 """
2997 default_bugs = []
2998 others = []
2999 for bug in bugs.split(','):
3000 bug = bug.strip()
3001 if bug:
3002 try:
3003 default_bugs.append(int(bug))
3004 except ValueError:
3005 others.append(bug)
3006
3007 if default_bugs:
3008 default_bugs = ','.join(map(str, default_bugs))
3009 if default_project:
3010 yield '%s:%s' % (default_project, default_bugs)
3011 else:
3012 yield default_bugs
3013 for other in sorted(others):
3014 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3015 yield other
3016
3017
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003018class ChangeDescription(object):
3019 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003020 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003021 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00003022 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003023 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003024
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003025 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003026 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003027
agable@chromium.org42c20792013-09-12 17:34:49 +00003028 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003029 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003030 return '\n'.join(self._description_lines)
3031
3032 def set_description(self, desc):
3033 if isinstance(desc, basestring):
3034 lines = desc.splitlines()
3035 else:
3036 lines = [line.rstrip() for line in desc]
3037 while lines and not lines[0]:
3038 lines.pop(0)
3039 while lines and not lines[-1]:
3040 lines.pop(-1)
3041 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003042
piman@chromium.org336f9122014-09-04 02:16:55 +00003043 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003044 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003045 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00003046 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003047 return
agable@chromium.org42c20792013-09-12 17:34:49 +00003048 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003049
agable@chromium.org42c20792013-09-12 17:34:49 +00003050 # Get the set of R= and TBR= lines and remove them from the desciption.
3051 regexp = re.compile(self.R_LINE)
3052 matches = [regexp.match(line) for line in self._description_lines]
3053 new_desc = [l for i, l in enumerate(self._description_lines)
3054 if not matches[i]]
3055 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003056
agable@chromium.org42c20792013-09-12 17:34:49 +00003057 # Construct new unified R= and TBR= lines.
3058 r_names = []
3059 tbr_names = []
3060 for match in matches:
3061 if not match:
3062 continue
3063 people = cleanup_list([match.group(2).strip()])
3064 if match.group(1) == 'TBR':
3065 tbr_names.extend(people)
3066 else:
3067 r_names.extend(people)
3068 for name in r_names:
3069 if name not in reviewers:
3070 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003071 if add_owners_tbr:
3072 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003073 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003074 all_reviewers = set(tbr_names + reviewers)
3075 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3076 all_reviewers)
3077 tbr_names.extend(owners_db.reviewers_for(missing_files,
3078 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003079 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3080 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3081
3082 # Put the new lines in the description where the old first R= line was.
3083 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3084 if 0 <= line_loc < len(self._description_lines):
3085 if new_tbr_line:
3086 self._description_lines.insert(line_loc, new_tbr_line)
3087 if new_r_line:
3088 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003089 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003090 if new_r_line:
3091 self.append_footer(new_r_line)
3092 if new_tbr_line:
3093 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003094
tandriif9aefb72016-07-01 09:06:51 -07003095 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003096 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003097 self.set_description([
3098 '# Enter a description of the change.',
3099 '# This will be displayed on the codereview site.',
3100 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003101 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003102 '--------------------',
3103 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003104
agable@chromium.org42c20792013-09-12 17:34:49 +00003105 regexp = re.compile(self.BUG_LINE)
3106 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003107 prefix = settings.GetBugPrefix()
3108 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
3109 for value in values:
3110 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
3111 self.append_footer('BUG=%s' % value)
3112
agable@chromium.org42c20792013-09-12 17:34:49 +00003113 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003114 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003115 if not content:
3116 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003117 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003118
3119 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003120 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3121 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003122 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003123 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003124
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003125 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003126 """Adds a footer line to the description.
3127
3128 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3129 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3130 that Gerrit footers are always at the end.
3131 """
3132 parsed_footer_line = git_footers.parse_footer(line)
3133 if parsed_footer_line:
3134 # Line is a gerrit footer in the form: Footer-Key: any value.
3135 # Thus, must be appended observing Gerrit footer rules.
3136 self.set_description(
3137 git_footers.add_footer(self.description,
3138 key=parsed_footer_line[0],
3139 value=parsed_footer_line[1]))
3140 return
3141
3142 if not self._description_lines:
3143 self._description_lines.append(line)
3144 return
3145
3146 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3147 if gerrit_footers:
3148 # git_footers.split_footers ensures that there is an empty line before
3149 # actual (gerrit) footers, if any. We have to keep it that way.
3150 assert top_lines and top_lines[-1] == ''
3151 top_lines, separator = top_lines[:-1], top_lines[-1:]
3152 else:
3153 separator = [] # No need for separator if there are no gerrit_footers.
3154
3155 prev_line = top_lines[-1] if top_lines else ''
3156 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3157 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3158 top_lines.append('')
3159 top_lines.append(line)
3160 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003161
tandrii99a72f22016-08-17 14:33:24 -07003162 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003163 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003164 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003165 reviewers = [match.group(2).strip()
3166 for match in matches
3167 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003168 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003169
bradnelsond975b302016-10-23 12:20:23 -07003170 def get_cced(self):
3171 """Retrieves the list of reviewers."""
3172 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3173 cced = [match.group(2).strip() for match in matches if match]
3174 return cleanup_list(cced)
3175
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003176 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3177 """Updates this commit description given the parent.
3178
3179 This is essentially what Gnumbd used to do.
3180 Consult https://goo.gl/WMmpDe for more details.
3181 """
3182 assert parent_msg # No, orphan branch creation isn't supported.
3183 assert parent_hash
3184 assert dest_ref
3185 parent_footer_map = git_footers.parse_footers(parent_msg)
3186 # This will also happily parse svn-position, which GnumbD is no longer
3187 # supporting. While we'd generate correct footers, the verifier plugin
3188 # installed in Gerrit will block such commit (ie git push below will fail).
3189 parent_position = git_footers.get_position(parent_footer_map)
3190
3191 # Cherry-picks may have last line obscuring their prior footers,
3192 # from git_footers perspective. This is also what Gnumbd did.
3193 cp_line = None
3194 if (self._description_lines and
3195 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3196 cp_line = self._description_lines.pop()
3197
3198 top_lines, _, parsed_footers = git_footers.split_footers(self.description)
3199
3200 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3201 # user interference with actual footers we'd insert below.
3202 for i, (k, v) in enumerate(parsed_footers):
3203 if k.startswith('Cr-'):
3204 parsed_footers[i] = (k.replace('Cr-', 'Cr-Original-'), v)
3205
3206 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003207 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003208 if parent_position[0] == dest_ref:
3209 # Same branch as parent.
3210 number = int(parent_position[1]) + 1
3211 else:
3212 number = 1 # New branch, and extra lineage.
3213 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3214 int(parent_position[1])))
3215
3216 parsed_footers.append(('Cr-Commit-Position',
3217 '%s@{#%d}' % (dest_ref, number)))
3218 parsed_footers.extend(('Cr-Branched-From', v) for v in lineage)
3219
3220 self._description_lines = top_lines
3221 if cp_line:
3222 self._description_lines.append(cp_line)
3223 if self._description_lines[-1] != '':
3224 self._description_lines.append('') # Ensure footer separator.
3225 self._description_lines.extend('%s: %s' % kv for kv in parsed_footers)
3226
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003227
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003228def get_approving_reviewers(props):
3229 """Retrieves the reviewers that approved a CL from the issue properties with
3230 messages.
3231
3232 Note that the list may contain reviewers that are not committer, thus are not
3233 considered by the CQ.
3234 """
3235 return sorted(
3236 set(
3237 message['sender']
3238 for message in props['messages']
3239 if message['approval'] and message['sender'] in props['reviewers']
3240 )
3241 )
3242
3243
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003244def FindCodereviewSettingsFile(filename='codereview.settings'):
3245 """Finds the given file starting in the cwd and going up.
3246
3247 Only looks up to the top of the repository unless an
3248 'inherit-review-settings-ok' file exists in the root of the repository.
3249 """
3250 inherit_ok_file = 'inherit-review-settings-ok'
3251 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003252 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003253 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3254 root = '/'
3255 while True:
3256 if filename in os.listdir(cwd):
3257 if os.path.isfile(os.path.join(cwd, filename)):
3258 return open(os.path.join(cwd, filename))
3259 if cwd == root:
3260 break
3261 cwd = os.path.dirname(cwd)
3262
3263
3264def LoadCodereviewSettingsFromFile(fileobj):
3265 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003266 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003267
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003268 def SetProperty(name, setting, unset_error_ok=False):
3269 fullname = 'rietveld.' + name
3270 if setting in keyvals:
3271 RunGit(['config', fullname, keyvals[setting]])
3272 else:
3273 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3274
tandrii48df5812016-10-17 03:55:37 -07003275 if not keyvals.get('GERRIT_HOST', False):
3276 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003277 # Only server setting is required. Other settings can be absent.
3278 # In that case, we ignore errors raised during option deletion attempt.
3279 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003280 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003281 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3282 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003283 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003284 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003285 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
3286 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003287 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003288 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003289 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003290 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3291 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003292
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003293 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003294 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003295
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003296 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003297 RunGit(['config', 'gerrit.squash-uploads',
3298 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003299
tandrii@chromium.org28253532016-04-14 13:46:56 +00003300 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003301 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003302 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3303
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003304 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3305 #should be of the form
3306 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3307 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
3308 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3309 keyvals['ORIGIN_URL_CONFIG']])
3310
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003311
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003312def urlretrieve(source, destination):
3313 """urllib is broken for SSL connections via a proxy therefore we
3314 can't use urllib.urlretrieve()."""
3315 with open(destination, 'w') as f:
3316 f.write(urllib2.urlopen(source).read())
3317
3318
ukai@chromium.org712d6102013-11-27 00:52:58 +00003319def hasSheBang(fname):
3320 """Checks fname is a #! script."""
3321 with open(fname) as f:
3322 return f.read(2).startswith('#!')
3323
3324
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003325# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3326def DownloadHooks(*args, **kwargs):
3327 pass
3328
3329
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003330def DownloadGerritHook(force):
3331 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003332
3333 Args:
3334 force: True to update hooks. False to install hooks if not present.
3335 """
3336 if not settings.GetIsGerrit():
3337 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003338 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003339 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3340 if not os.access(dst, os.X_OK):
3341 if os.path.exists(dst):
3342 if not force:
3343 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003344 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003345 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003346 if not hasSheBang(dst):
3347 DieWithError('Not a script: %s\n'
3348 'You need to download from\n%s\n'
3349 'into .git/hooks/commit-msg and '
3350 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003351 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3352 except Exception:
3353 if os.path.exists(dst):
3354 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003355 DieWithError('\nFailed to download hooks.\n'
3356 'You need to download from\n%s\n'
3357 'into .git/hooks/commit-msg and '
3358 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003359
3360
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003361
3362def GetRietveldCodereviewSettingsInteractively():
3363 """Prompt the user for settings."""
3364 server = settings.GetDefaultServerUrl(error_ok=True)
3365 prompt = 'Rietveld server (host[:port])'
3366 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3367 newserver = ask_for_data(prompt + ':')
3368 if not server and not newserver:
3369 newserver = DEFAULT_SERVER
3370 if newserver:
3371 newserver = gclient_utils.UpgradeToHttps(newserver)
3372 if newserver != server:
3373 RunGit(['config', 'rietveld.server', newserver])
3374
3375 def SetProperty(initial, caption, name, is_url):
3376 prompt = caption
3377 if initial:
3378 prompt += ' ("x" to clear) [%s]' % initial
3379 new_val = ask_for_data(prompt + ':')
3380 if new_val == 'x':
3381 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3382 elif new_val:
3383 if is_url:
3384 new_val = gclient_utils.UpgradeToHttps(new_val)
3385 if new_val != initial:
3386 RunGit(['config', 'rietveld.' + name, new_val])
3387
3388 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3389 SetProperty(settings.GetDefaultPrivateFlag(),
3390 'Private flag (rietveld only)', 'private', False)
3391 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3392 'tree-status-url', False)
3393 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3394 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3395 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3396 'run-post-upload-hook', False)
3397
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003398@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003399def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003400 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003401
tandrii5d0a0422016-09-14 06:24:35 -07003402 print('WARNING: git cl config works for Rietveld only')
3403 # TODO(tandrii): remove this once we switch to Gerrit.
3404 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003405 parser.add_option('--activate-update', action='store_true',
3406 help='activate auto-updating [rietveld] section in '
3407 '.git/config')
3408 parser.add_option('--deactivate-update', action='store_true',
3409 help='deactivate auto-updating [rietveld] section in '
3410 '.git/config')
3411 options, args = parser.parse_args(args)
3412
3413 if options.deactivate_update:
3414 RunGit(['config', 'rietveld.autoupdate', 'false'])
3415 return
3416
3417 if options.activate_update:
3418 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3419 return
3420
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003421 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003422 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003423 return 0
3424
3425 url = args[0]
3426 if not url.endswith('codereview.settings'):
3427 url = os.path.join(url, 'codereview.settings')
3428
3429 # Load code review settings and download hooks (if available).
3430 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3431 return 0
3432
3433
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003434def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003435 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003436 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3437 branch = ShortBranchName(branchref)
3438 _, args = parser.parse_args(args)
3439 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003440 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003441 return RunGit(['config', 'branch.%s.base-url' % branch],
3442 error_ok=False).strip()
3443 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003444 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003445 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3446 error_ok=False).strip()
3447
3448
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003449def color_for_status(status):
3450 """Maps a Changelist status to color, for CMDstatus and other tools."""
3451 return {
3452 'unsent': Fore.RED,
3453 'waiting': Fore.BLUE,
3454 'reply': Fore.YELLOW,
3455 'lgtm': Fore.GREEN,
3456 'commit': Fore.MAGENTA,
3457 'closed': Fore.CYAN,
3458 'error': Fore.WHITE,
3459 }.get(status, Fore.WHITE)
3460
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003461
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003462def get_cl_statuses(changes, fine_grained, max_processes=None):
3463 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003464
3465 If fine_grained is true, this will fetch CL statuses from the server.
3466 Otherwise, simply indicate if there's a matching url for the given branches.
3467
3468 If max_processes is specified, it is used as the maximum number of processes
3469 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3470 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003471
3472 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003473 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003474 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003475 upload.verbosity = 0
3476
3477 if fine_grained:
3478 # Process one branch synchronously to work through authentication, then
3479 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003480 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003481 def fetch(cl):
3482 try:
3483 return (cl, cl.GetStatus())
3484 except:
3485 # See http://crbug.com/629863.
3486 logging.exception('failed to fetch status for %s:', cl)
3487 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003488 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003489
tandriiea9514a2016-08-17 12:32:37 -07003490 changes_to_fetch = changes[1:]
3491 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003492 # Exit early if there was only one branch to fetch.
3493 return
3494
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003495 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003496 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003497 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003498 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003499
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003500 fetched_cls = set()
3501 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003502 while True:
3503 try:
3504 row = it.next(timeout=5)
3505 except multiprocessing.TimeoutError:
3506 break
3507
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003508 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003509 yield row
3510
3511 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003512 for cl in set(changes_to_fetch) - fetched_cls:
3513 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003514
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003515 else:
3516 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003517 for cl in changes:
3518 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003519
rmistry@google.com2dd99862015-06-22 12:22:18 +00003520
3521def upload_branch_deps(cl, args):
3522 """Uploads CLs of local branches that are dependents of the current branch.
3523
3524 If the local branch dependency tree looks like:
3525 test1 -> test2.1 -> test3.1
3526 -> test3.2
3527 -> test2.2 -> test3.3
3528
3529 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3530 run on the dependent branches in this order:
3531 test2.1, test3.1, test3.2, test2.2, test3.3
3532
3533 Note: This function does not rebase your local dependent branches. Use it when
3534 you make a change to the parent branch that will not conflict with its
3535 dependent branches, and you would like their dependencies updated in
3536 Rietveld.
3537 """
3538 if git_common.is_dirty_git_tree('upload-branch-deps'):
3539 return 1
3540
3541 root_branch = cl.GetBranch()
3542 if root_branch is None:
3543 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3544 'Get on a branch!')
3545 if not cl.GetIssue() or not cl.GetPatchset():
3546 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3547 'patchset dependencies without an uploaded CL.')
3548
3549 branches = RunGit(['for-each-ref',
3550 '--format=%(refname:short) %(upstream:short)',
3551 'refs/heads'])
3552 if not branches:
3553 print('No local branches found.')
3554 return 0
3555
3556 # Create a dictionary of all local branches to the branches that are dependent
3557 # on it.
3558 tracked_to_dependents = collections.defaultdict(list)
3559 for b in branches.splitlines():
3560 tokens = b.split()
3561 if len(tokens) == 2:
3562 branch_name, tracked = tokens
3563 tracked_to_dependents[tracked].append(branch_name)
3564
vapiera7fbd5a2016-06-16 09:17:49 -07003565 print()
3566 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003567 dependents = []
3568 def traverse_dependents_preorder(branch, padding=''):
3569 dependents_to_process = tracked_to_dependents.get(branch, [])
3570 padding += ' '
3571 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003572 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003573 dependents.append(dependent)
3574 traverse_dependents_preorder(dependent, padding)
3575 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003576 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003577
3578 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003579 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003580 return 0
3581
vapiera7fbd5a2016-06-16 09:17:49 -07003582 print('This command will checkout all dependent branches and run '
3583 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003584 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3585
andybons@chromium.org962f9462016-02-03 20:00:42 +00003586 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003587 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003588 args.extend(['-t', 'Updated patchset dependency'])
3589
rmistry@google.com2dd99862015-06-22 12:22:18 +00003590 # Record all dependents that failed to upload.
3591 failures = {}
3592 # Go through all dependents, checkout the branch and upload.
3593 try:
3594 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003595 print()
3596 print('--------------------------------------')
3597 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003598 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003599 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003600 try:
3601 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003602 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003603 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003604 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003605 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003606 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003607 finally:
3608 # Swap back to the original root branch.
3609 RunGit(['checkout', '-q', root_branch])
3610
vapiera7fbd5a2016-06-16 09:17:49 -07003611 print()
3612 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003613 for dependent_branch in dependents:
3614 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003615 print(' %s : %s' % (dependent_branch, upload_status))
3616 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003617
3618 return 0
3619
3620
kmarshall3bff56b2016-06-06 18:31:47 -07003621def CMDarchive(parser, args):
3622 """Archives and deletes branches associated with closed changelists."""
3623 parser.add_option(
3624 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003625 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003626 parser.add_option(
3627 '-f', '--force', action='store_true',
3628 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003629 parser.add_option(
3630 '-d', '--dry-run', action='store_true',
3631 help='Skip the branch tagging and removal steps.')
3632 parser.add_option(
3633 '-t', '--notags', action='store_true',
3634 help='Do not tag archived branches. '
3635 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003636
3637 auth.add_auth_options(parser)
3638 options, args = parser.parse_args(args)
3639 if args:
3640 parser.error('Unsupported args: %s' % ' '.join(args))
3641 auth_config = auth.extract_auth_config_from_options(options)
3642
3643 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3644 if not branches:
3645 return 0
3646
vapiera7fbd5a2016-06-16 09:17:49 -07003647 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003648 changes = [Changelist(branchref=b, auth_config=auth_config)
3649 for b in branches.splitlines()]
3650 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3651 statuses = get_cl_statuses(changes,
3652 fine_grained=True,
3653 max_processes=options.maxjobs)
3654 proposal = [(cl.GetBranch(),
3655 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3656 for cl, status in statuses
3657 if status == 'closed']
3658 proposal.sort()
3659
3660 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003661 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003662 return 0
3663
3664 current_branch = GetCurrentBranch()
3665
vapiera7fbd5a2016-06-16 09:17:49 -07003666 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003667 if options.notags:
3668 for next_item in proposal:
3669 print(' ' + next_item[0])
3670 else:
3671 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3672 for next_item in proposal:
3673 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003674
kmarshall9249e012016-08-23 12:02:16 -07003675 # Quit now on precondition failure or if instructed by the user, either
3676 # via an interactive prompt or by command line flags.
3677 if options.dry_run:
3678 print('\nNo changes were made (dry run).\n')
3679 return 0
3680 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003681 print('You are currently on a branch \'%s\' which is associated with a '
3682 'closed codereview issue, so archive cannot proceed. Please '
3683 'checkout another branch and run this command again.' %
3684 current_branch)
3685 return 1
kmarshall9249e012016-08-23 12:02:16 -07003686 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003687 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3688 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003689 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003690 return 1
3691
3692 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003693 if not options.notags:
3694 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003695 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003696
vapiera7fbd5a2016-06-16 09:17:49 -07003697 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003698
3699 return 0
3700
3701
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003702def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003703 """Show status of changelists.
3704
3705 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003706 - Red not sent for review or broken
3707 - Blue waiting for review
3708 - Yellow waiting for you to reply to review
3709 - Green LGTM'ed
3710 - Magenta in the commit queue
3711 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003712
3713 Also see 'git cl comments'.
3714 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003715 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003716 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003717 parser.add_option('-f', '--fast', action='store_true',
3718 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003719 parser.add_option(
3720 '-j', '--maxjobs', action='store', type=int,
3721 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003722
3723 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003724 _add_codereview_issue_select_options(
3725 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003726 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003727 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003728 if args:
3729 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003730 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003731
iannuccie53c9352016-08-17 14:40:40 -07003732 if options.issue is not None and not options.field:
3733 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003734
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003735 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003736 cl = Changelist(auth_config=auth_config, issue=options.issue,
3737 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003738 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003739 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003740 elif options.field == 'id':
3741 issueid = cl.GetIssue()
3742 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003743 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003744 elif options.field == 'patch':
3745 patchset = cl.GetPatchset()
3746 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003747 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003748 elif options.field == 'status':
3749 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003750 elif options.field == 'url':
3751 url = cl.GetIssueURL()
3752 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003753 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003754 return 0
3755
3756 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3757 if not branches:
3758 print('No local branch found.')
3759 return 0
3760
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003761 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003762 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003763 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003764 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003765 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003766 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003767 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003768
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003769 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003770 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3771 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3772 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003773 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003774 c, status = output.next()
3775 branch_statuses[c.GetBranch()] = status
3776 status = branch_statuses.pop(branch)
3777 url = cl.GetIssueURL()
3778 if url and (not status or status == 'error'):
3779 # The issue probably doesn't exist anymore.
3780 url += ' (broken)'
3781
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003782 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003783 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003784 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003785 color = ''
3786 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003787 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003788 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003789 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003790 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003791
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003792 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003793 print()
3794 print('Current branch:',)
3795 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003796 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003797 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003798 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003799 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003800 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003801 print('Issue description:')
3802 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003803 return 0
3804
3805
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003806def colorize_CMDstatus_doc():
3807 """To be called once in main() to add colors to git cl status help."""
3808 colors = [i for i in dir(Fore) if i[0].isupper()]
3809
3810 def colorize_line(line):
3811 for color in colors:
3812 if color in line.upper():
3813 # Extract whitespaces first and the leading '-'.
3814 indent = len(line) - len(line.lstrip(' ')) + 1
3815 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3816 return line
3817
3818 lines = CMDstatus.__doc__.splitlines()
3819 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3820
3821
phajdan.jre328cf92016-08-22 04:12:17 -07003822def write_json(path, contents):
3823 with open(path, 'w') as f:
3824 json.dump(contents, f)
3825
3826
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003827@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003828def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003829 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003830
3831 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003832 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003833 parser.add_option('-r', '--reverse', action='store_true',
3834 help='Lookup the branch(es) for the specified issues. If '
3835 'no issues are specified, all branches with mapped '
3836 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003837 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003838 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003839 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003840 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003841
dnj@chromium.org406c4402015-03-03 17:22:28 +00003842 if options.reverse:
3843 branches = RunGit(['for-each-ref', 'refs/heads',
3844 '--format=%(refname:short)']).splitlines()
3845
3846 # Reverse issue lookup.
3847 issue_branch_map = {}
3848 for branch in branches:
3849 cl = Changelist(branchref=branch)
3850 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3851 if not args:
3852 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003853 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003854 for issue in args:
3855 if not issue:
3856 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003857 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003858 print('Branch for issue number %s: %s' % (
3859 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003860 if options.json:
3861 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003862 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003863 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003864 if len(args) > 0:
3865 try:
3866 issue = int(args[0])
3867 except ValueError:
3868 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003869 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003870 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003871 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003872 if options.json:
3873 write_json(options.json, {
3874 'issue': cl.GetIssue(),
3875 'issue_url': cl.GetIssueURL(),
3876 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003877 return 0
3878
3879
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003880def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003881 """Shows or posts review comments for any changelist."""
3882 parser.add_option('-a', '--add-comment', dest='comment',
3883 help='comment to add to an issue')
3884 parser.add_option('-i', dest='issue',
3885 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003886 parser.add_option('-j', '--json-file',
3887 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003888 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003889 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003890 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003891
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003892 issue = None
3893 if options.issue:
3894 try:
3895 issue = int(options.issue)
3896 except ValueError:
3897 DieWithError('A review issue id is expected to be a number')
3898
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003899 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003900
3901 if options.comment:
3902 cl.AddComment(options.comment)
3903 return 0
3904
3905 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003906 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003907 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003908 summary.append({
3909 'date': message['date'],
3910 'lgtm': False,
3911 'message': message['text'],
3912 'not_lgtm': False,
3913 'sender': message['sender'],
3914 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003915 if message['disapproval']:
3916 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003917 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003918 elif message['approval']:
3919 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003920 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003921 elif message['sender'] == data['owner_email']:
3922 color = Fore.MAGENTA
3923 else:
3924 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003925 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003926 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003927 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003928 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003929 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003930 if options.json_file:
3931 with open(options.json_file, 'wb') as f:
3932 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003933 return 0
3934
3935
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003936@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003937def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003938 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003939 parser.add_option('-d', '--display', action='store_true',
3940 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003941 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003942 help='New description to set for this issue (- for stdin, '
3943 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003944 parser.add_option('-f', '--force', action='store_true',
3945 help='Delete any unpublished Gerrit edits for this issue '
3946 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003947
3948 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003949 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003950 options, args = parser.parse_args(args)
3951 _process_codereview_select_options(parser, options)
3952
3953 target_issue = None
3954 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003955 target_issue = ParseIssueNumberArgument(args[0])
3956 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003957 parser.print_help()
3958 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003959
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003960 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003961
martiniss6eda05f2016-06-30 10:18:35 -07003962 kwargs = {
3963 'auth_config': auth_config,
3964 'codereview': options.forced_codereview,
3965 }
3966 if target_issue:
3967 kwargs['issue'] = target_issue.issue
3968 if options.forced_codereview == 'rietveld':
3969 kwargs['rietveld_server'] = target_issue.hostname
3970
3971 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003972
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003973 if not cl.GetIssue():
3974 DieWithError('This branch has no associated changelist.')
3975 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003976
smut@google.com34fb6b12015-07-13 20:03:26 +00003977 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003978 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003979 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003980
3981 if options.new_description:
3982 text = options.new_description
3983 if text == '-':
3984 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003985 elif text == '+':
3986 base_branch = cl.GetCommonAncestorWithUpstream()
3987 change = cl.GetChange(base_branch, None, local_description=True)
3988 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003989
3990 description.set_description(text)
3991 else:
3992 description.prompt()
3993
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003994 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003995 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003996 return 0
3997
3998
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003999def CreateDescriptionFromLog(args):
4000 """Pulls out the commit log to use as a base for the CL description."""
4001 log_args = []
4002 if len(args) == 1 and not args[0].endswith('.'):
4003 log_args = [args[0] + '..']
4004 elif len(args) == 1 and args[0].endswith('...'):
4005 log_args = [args[0][:-1]]
4006 elif len(args) == 2:
4007 log_args = [args[0] + '..' + args[1]]
4008 else:
4009 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004010 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004011
4012
thestig@chromium.org44202a22014-03-11 19:22:18 +00004013def CMDlint(parser, args):
4014 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004015 parser.add_option('--filter', action='append', metavar='-x,+y',
4016 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004017 auth.add_auth_options(parser)
4018 options, args = parser.parse_args(args)
4019 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004020
4021 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004022 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004023 try:
4024 import cpplint
4025 import cpplint_chromium
4026 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004027 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004028 return 1
4029
4030 # Change the current working directory before calling lint so that it
4031 # shows the correct base.
4032 previous_cwd = os.getcwd()
4033 os.chdir(settings.GetRoot())
4034 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004035 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004036 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4037 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004038 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004039 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004040 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004041
4042 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004043 command = args + files
4044 if options.filter:
4045 command = ['--filter=' + ','.join(options.filter)] + command
4046 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004047
4048 white_regex = re.compile(settings.GetLintRegex())
4049 black_regex = re.compile(settings.GetLintIgnoreRegex())
4050 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4051 for filename in filenames:
4052 if white_regex.match(filename):
4053 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004054 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004055 else:
4056 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4057 extra_check_functions)
4058 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004059 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004060 finally:
4061 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004062 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004063 if cpplint._cpplint_state.error_count != 0:
4064 return 1
4065 return 0
4066
4067
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004068def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004069 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004070 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004071 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004072 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004073 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004074 auth.add_auth_options(parser)
4075 options, args = parser.parse_args(args)
4076 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004077
sbc@chromium.org71437c02015-04-09 19:29:40 +00004078 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004079 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004080 return 1
4081
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004082 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004083 if args:
4084 base_branch = args[0]
4085 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004086 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004087 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004088
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004089 cl.RunHook(
4090 committing=not options.upload,
4091 may_prompt=False,
4092 verbose=options.verbose,
4093 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004094 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004095
4096
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004097def GenerateGerritChangeId(message):
4098 """Returns Ixxxxxx...xxx change id.
4099
4100 Works the same way as
4101 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4102 but can be called on demand on all platforms.
4103
4104 The basic idea is to generate git hash of a state of the tree, original commit
4105 message, author/committer info and timestamps.
4106 """
4107 lines = []
4108 tree_hash = RunGitSilent(['write-tree'])
4109 lines.append('tree %s' % tree_hash.strip())
4110 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4111 if code == 0:
4112 lines.append('parent %s' % parent.strip())
4113 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4114 lines.append('author %s' % author.strip())
4115 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4116 lines.append('committer %s' % committer.strip())
4117 lines.append('')
4118 # Note: Gerrit's commit-hook actually cleans message of some lines and
4119 # whitespace. This code is not doing this, but it clearly won't decrease
4120 # entropy.
4121 lines.append(message)
4122 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4123 stdin='\n'.join(lines))
4124 return 'I%s' % change_hash.strip()
4125
4126
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004127def GetTargetRef(remote, remote_branch, target_branch, pending_prefix_check,
4128 remote_url=None):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004129 """Computes the remote branch ref to use for the CL.
4130
4131 Args:
4132 remote (str): The git remote for the CL.
4133 remote_branch (str): The git remote branch for the CL.
4134 target_branch (str): The target branch specified by the user.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004135 pending_prefix_check (bool): If true, determines if pending_prefix should be
4136 used.
4137 remote_url (str): Only used for checking if pending_prefix should be used.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004138 """
4139 if not (remote and remote_branch):
4140 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004141
wittman@chromium.org455dc922015-01-26 20:15:50 +00004142 if target_branch:
4143 # Cannonicalize branch references to the equivalent local full symbolic
4144 # refs, which are then translated into the remote full symbolic refs
4145 # below.
4146 if '/' not in target_branch:
4147 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4148 else:
4149 prefix_replacements = (
4150 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4151 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4152 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4153 )
4154 match = None
4155 for regex, replacement in prefix_replacements:
4156 match = re.search(regex, target_branch)
4157 if match:
4158 remote_branch = target_branch.replace(match.group(0), replacement)
4159 break
4160 if not match:
4161 # This is a branch path but not one we recognize; use as-is.
4162 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004163 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4164 # Handle the refs that need to land in different refs.
4165 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004166
wittman@chromium.org455dc922015-01-26 20:15:50 +00004167 # Create the true path to the remote branch.
4168 # Does the following translation:
4169 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4170 # * refs/remotes/origin/master -> refs/heads/master
4171 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4172 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4173 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4174 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4175 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4176 'refs/heads/')
4177 elif remote_branch.startswith('refs/remotes/branch-heads'):
4178 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004179
4180 if pending_prefix_check:
4181 # If a pending prefix exists then replace refs/ with it.
4182 state = _GitNumbererState.load(remote_url, remote_branch)
4183 if state.pending_prefix:
4184 remote_branch = remote_branch.replace('refs/', state.pending_prefix)
wittman@chromium.org455dc922015-01-26 20:15:50 +00004185 return remote_branch
4186
4187
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004188def cleanup_list(l):
4189 """Fixes a list so that comma separated items are put as individual items.
4190
4191 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4192 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4193 """
4194 items = sum((i.split(',') for i in l), [])
4195 stripped_items = (i.strip() for i in items)
4196 return sorted(filter(None, stripped_items))
4197
4198
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004199@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004200def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004201 """Uploads the current changelist to codereview.
4202
4203 Can skip dependency patchset uploads for a branch by running:
4204 git config branch.branch_name.skip-deps-uploads True
4205 To unset run:
4206 git config --unset branch.branch_name.skip-deps-uploads
4207 Can also set the above globally by using the --global flag.
4208 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004209 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4210 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004211 parser.add_option('--bypass-watchlists', action='store_true',
4212 dest='bypass_watchlists',
4213 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004214 parser.add_option('-f', action='store_true', dest='force',
4215 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004216 parser.add_option('--message', '-m', dest='message',
4217 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004218 parser.add_option('-b', '--bug',
4219 help='pre-populate the bug number(s) for this issue. '
4220 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004221 parser.add_option('--message-file', dest='message_file',
4222 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004223 parser.add_option('--title', '-t', dest='title',
4224 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004225 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004226 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004227 help='reviewer email addresses')
4228 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004229 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004230 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004231 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004232 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004233 parser.add_option('--emulate_svn_auto_props',
4234 '--emulate-svn-auto-props',
4235 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004236 dest="emulate_svn_auto_props",
4237 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004238 parser.add_option('-c', '--use-commit-queue', action='store_true',
4239 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004240 parser.add_option('--private', action='store_true',
4241 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004242 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004243 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004244 metavar='TARGET',
4245 help='Apply CL to remote ref TARGET. ' +
4246 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004247 parser.add_option('--squash', action='store_true',
4248 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004249 parser.add_option('--no-squash', action='store_true',
4250 help='Don\'t squash multiple commits into one ' +
4251 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004252 parser.add_option('--topic', default=None,
4253 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004254 parser.add_option('--email', default=None,
4255 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004256 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4257 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004258 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4259 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004260 help='Send the patchset to do a CQ dry run right after '
4261 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004262 parser.add_option('--dependencies', action='store_true',
4263 help='Uploads CLs of all the local branches that depend on '
4264 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004265
rmistry@google.com2dd99862015-06-22 12:22:18 +00004266 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004267 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004268 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004269 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004270 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004271 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004272 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004273
sbc@chromium.org71437c02015-04-09 19:29:40 +00004274 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004275 return 1
4276
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004277 options.reviewers = cleanup_list(options.reviewers)
4278 options.cc = cleanup_list(options.cc)
4279
tandriib80458a2016-06-23 12:20:07 -07004280 if options.message_file:
4281 if options.message:
4282 parser.error('only one of --message and --message-file allowed.')
4283 options.message = gclient_utils.FileRead(options.message_file)
4284 options.message_file = None
4285
tandrii4d0545a2016-07-06 03:56:49 -07004286 if options.cq_dry_run and options.use_commit_queue:
4287 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4288
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004289 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4290 settings.GetIsGerrit()
4291
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004292 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004293 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004294
4295
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004296def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004297 print()
4298 print('Waiting for commit to be landed on %s...' % real_ref)
4299 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004300 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4301 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004302 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004303
4304 loop = 0
4305 while True:
4306 sys.stdout.write('fetching (%d)... \r' % loop)
4307 sys.stdout.flush()
4308 loop += 1
4309
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004310 if mirror:
4311 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004312 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4313 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4314 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4315 for commit in commits.splitlines():
4316 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004317 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004318 return commit
4319
4320 current_rev = to_rev
4321
4322
tandriibf429402016-09-14 07:09:12 -07004323def PushToGitPending(remote, pending_ref):
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004324 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4325
4326 Returns:
4327 (retcode of last operation, output log of last operation).
4328 """
4329 assert pending_ref.startswith('refs/'), pending_ref
4330 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4331 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4332 code = 0
4333 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004334 max_attempts = 3
4335 attempts_left = max_attempts
4336 while attempts_left:
4337 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004338 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004339 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004340
4341 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004342 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004343 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004344 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004345 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004346 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004347 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004348 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004349 continue
4350
4351 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004352 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004353 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004354 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004355 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004356 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4357 'the following files have merge conflicts:' % pending_ref)
4358 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4359 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004360 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004361 return code, out
4362
4363 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004364 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004365 code, out = RunGitWithCode(
4366 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4367 if code == 0:
4368 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004369 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004370 return code, out
4371
vapiera7fbd5a2016-06-16 09:17:49 -07004372 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004373 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004374 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004375 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004376 print('Fatal push error. Make sure your .netrc credentials and git '
4377 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004378 return code, out
4379
vapiera7fbd5a2016-06-16 09:17:49 -07004380 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004381 return code, out
4382
4383
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004384def IsFatalPushFailure(push_stdout):
4385 """True if retrying push won't help."""
4386 return '(prohibited by Gerrit)' in push_stdout
4387
4388
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004389@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004390def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004391 """DEPRECATED: Used to commit the current changelist via git-svn."""
4392 message = ('git-cl no longer supports committing to SVN repositories via '
4393 'git-svn. You probably want to use `git cl land` instead.')
4394 print(message)
4395 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004396
4397
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004398@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004399def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004400 """Commits the current changelist via git.
4401
4402 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4403 upstream and closes the issue automatically and atomically.
4404
4405 Otherwise (in case of Rietveld):
4406 Squashes branch into a single commit.
4407 Updates commit message with metadata (e.g. pointer to review).
4408 Pushes the code upstream.
4409 Updates review and closes.
4410 """
4411 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4412 help='bypass upload presubmit hook')
4413 parser.add_option('-m', dest='message',
4414 help="override review description")
4415 parser.add_option('-f', action='store_true', dest='force',
4416 help="force yes to questions (don't prompt)")
4417 parser.add_option('-c', dest='contributor',
4418 help="external contributor for patch (appended to " +
4419 "description and used as author for git). Should be " +
4420 "formatted as 'First Last <email@example.com>'")
4421 add_git_similarity(parser)
4422 auth.add_auth_options(parser)
4423 (options, args) = parser.parse_args(args)
4424 auth_config = auth.extract_auth_config_from_options(options)
4425
4426 cl = Changelist(auth_config=auth_config)
4427
4428 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4429 if cl.IsGerrit():
4430 if options.message:
4431 # This could be implemented, but it requires sending a new patch to
4432 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4433 # Besides, Gerrit has the ability to change the commit message on submit
4434 # automatically, thus there is no need to support this option (so far?).
4435 parser.error('-m MESSAGE option is not supported for Gerrit.')
4436 if options.contributor:
4437 parser.error(
4438 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4439 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4440 'the contributor\'s "name <email>". If you can\'t upload such a '
4441 'commit for review, contact your repository admin and request'
4442 '"Forge-Author" permission.')
4443 if not cl.GetIssue():
4444 DieWithError('You must upload the change first to Gerrit.\n'
4445 ' If you would rather have `git cl land` upload '
4446 'automatically for you, see http://crbug.com/642759')
4447 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4448 options.verbose)
4449
4450 current = cl.GetBranch()
4451 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4452 if remote == '.':
4453 print()
4454 print('Attempting to push branch %r into another local branch!' % current)
4455 print()
4456 print('Either reparent this branch on top of origin/master:')
4457 print(' git reparent-branch --root')
4458 print()
4459 print('OR run `git rebase-update` if you think the parent branch is ')
4460 print('already committed.')
4461 print()
4462 print(' Current parent: %r' % upstream_branch)
4463 return 1
4464
4465 if not args:
4466 # Default to merging against our best guess of the upstream branch.
4467 args = [cl.GetUpstreamBranch()]
4468
4469 if options.contributor:
4470 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
4471 print("Please provide contibutor as 'First Last <email@example.com>'")
4472 return 1
4473
4474 base_branch = args[0]
4475
4476 if git_common.is_dirty_git_tree('land'):
4477 return 1
4478
4479 # This rev-list syntax means "show all commits not in my branch that
4480 # are in base_branch".
4481 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4482 base_branch]).splitlines()
4483 if upstream_commits:
4484 print('Base branch "%s" has %d commits '
4485 'not in this branch.' % (base_branch, len(upstream_commits)))
4486 print('Run "git merge %s" before attempting to land.' % base_branch)
4487 return 1
4488
4489 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
4490 if not options.bypass_hooks:
4491 author = None
4492 if options.contributor:
4493 author = re.search(r'\<(.*)\>', options.contributor).group(1)
4494 hook_results = cl.RunHook(
4495 committing=True,
4496 may_prompt=not options.force,
4497 verbose=options.verbose,
4498 change=cl.GetChange(merge_base, author))
4499 if not hook_results.should_continue():
4500 return 1
4501
4502 # Check the tree status if the tree status URL is set.
4503 status = GetTreeStatus()
4504 if 'closed' == status:
4505 print('The tree is closed. Please wait for it to reopen. Use '
4506 '"git cl land --bypass-hooks" to commit on a closed tree.')
4507 return 1
4508 elif 'unknown' == status:
4509 print('Unable to determine tree status. Please verify manually and '
4510 'use "git cl land --bypass-hooks" to commit on a closed tree.')
4511 return 1
4512
4513 change_desc = ChangeDescription(options.message)
4514 if not change_desc.description and cl.GetIssue():
4515 change_desc = ChangeDescription(cl.GetDescription())
4516
4517 if not change_desc.description:
4518 if not cl.GetIssue() and options.bypass_hooks:
4519 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
4520 else:
4521 print('No description set.')
4522 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
4523 return 1
4524
4525 # Keep a separate copy for the commit message, because the commit message
4526 # contains the link to the Rietveld issue, while the Rietveld message contains
4527 # the commit viewvc url.
4528 if cl.GetIssue():
4529 change_desc.update_reviewers(cl.GetApprovingReviewers())
4530
4531 commit_desc = ChangeDescription(change_desc.description)
4532 if cl.GetIssue():
4533 # Xcode won't linkify this URL unless there is a non-whitespace character
4534 # after it. Add a period on a new line to circumvent this. Also add a space
4535 # before the period to make sure that Gitiles continues to correctly resolve
4536 # the URL.
4537 commit_desc.append_footer('Review-Url: %s .' % cl.GetIssueURL())
4538 if options.contributor:
4539 commit_desc.append_footer('Patch from %s.' % options.contributor)
4540
4541 print('Description:')
4542 print(commit_desc.description)
4543
4544 branches = [merge_base, cl.GetBranchRef()]
4545 if not options.force:
4546 print_stats(options.similarity, options.find_copies, branches)
4547
4548 # We want to squash all this branch's commits into one commit with the proper
4549 # description. We do this by doing a "reset --soft" to the base branch (which
4550 # keeps the working copy the same), then landing that.
4551 MERGE_BRANCH = 'git-cl-commit'
4552 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4553 # Delete the branches if they exist.
4554 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4555 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4556 result = RunGitWithCode(showref_cmd)
4557 if result[0] == 0:
4558 RunGit(['branch', '-D', branch])
4559
4560 # We might be in a directory that's present in this branch but not in the
4561 # trunk. Move up to the top of the tree so that git commands that expect a
4562 # valid CWD won't fail after we check out the merge branch.
4563 rel_base_path = settings.GetRelativeRoot()
4564 if rel_base_path:
4565 os.chdir(rel_base_path)
4566
4567 # Stuff our change into the merge branch.
4568 # We wrap in a try...finally block so if anything goes wrong,
4569 # we clean up the branches.
4570 retcode = -1
4571 pushed_to_pending = False
4572 pending_ref = None
4573 revision = None
4574 try:
4575 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
4576 RunGit(['reset', '--soft', merge_base])
4577 if options.contributor:
4578 RunGit(
4579 [
4580 'commit', '--author', options.contributor,
4581 '-m', commit_desc.description,
4582 ])
4583 else:
4584 RunGit(['commit', '-m', commit_desc.description])
4585
4586 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
4587 mirror = settings.GetGitMirror(remote)
4588 if mirror:
4589 pushurl = mirror.url
4590 git_numberer = _GitNumbererState.load(pushurl, branch)
4591 else:
4592 pushurl = remote # Usually, this is 'origin'.
4593 git_numberer = _GitNumbererState.load(
4594 RunGit(['config', 'remote.%s.url' % remote]).strip(), branch)
4595
4596 if git_numberer.should_add_git_number:
4597 # TODO(tandrii): run git fetch in a loop + autorebase when there there
4598 # is no pending ref to push to?
4599 logging.debug('Adding git number footers')
4600 parent_msg = RunGit(['show', '-s', '--format=%B', merge_base]).strip()
4601 commit_desc.update_with_git_number_footers(merge_base, parent_msg,
4602 branch)
4603 # Ensure timestamps are monotonically increasing.
4604 timestamp = max(1 + _get_committer_timestamp(merge_base),
4605 _get_committer_timestamp('HEAD'))
4606 _git_amend_head(commit_desc.description, timestamp)
4607 change_desc = ChangeDescription(commit_desc.description)
4608 # If gnumbd is sitll ON and we ultimately push to branch with
4609 # pending_prefix, gnumbd will modify footers we've just inserted with
4610 # 'Original-', which is annoying but still technically correct.
4611
4612 pending_prefix = git_numberer.pending_prefix
4613 if not pending_prefix or branch.startswith(pending_prefix):
4614 # If not using refs/pending/heads/* at all, or target ref is already set
4615 # to pending, then push to the target ref directly.
4616 # NB(tandrii): I think branch.startswith(pending_prefix) never happens
4617 # in practise. I really tried to create a new branch tracking
4618 # refs/pending/heads/master directly and git cl land failed long before
4619 # reaching this. Disagree? Comment on http://crbug.com/642493.
4620 if pending_prefix:
4621 print('\n\nYOU GOT A CHANCE TO WIN A FREE GIFT!\n\n'
4622 'Grab your .git/config, add instructions how to reproduce '
4623 'this, and post it to http://crbug.com/642493.\n'
4624 'The first reporter gets a free "Black Swan" book from '
4625 'tandrii@\n\n')
4626 retcode, output = RunGitWithCode(
4627 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
4628 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
4629 else:
4630 # Cherry-pick the change on top of pending ref and then push it.
4631 assert branch.startswith('refs/'), branch
4632 assert pending_prefix[-1] == '/', pending_prefix
4633 pending_ref = pending_prefix + branch[len('refs/'):]
4634 retcode, output = PushToGitPending(pushurl, pending_ref)
4635 pushed_to_pending = (retcode == 0)
4636
4637 if retcode == 0:
4638 revision = RunGit(['rev-parse', 'HEAD']).strip()
4639 logging.debug(output)
4640 except: # pylint: disable=bare-except
4641 if _IS_BEING_TESTED:
4642 logging.exception('this is likely your ACTUAL cause of test failure.\n'
4643 + '-' * 30 + '8<' + '-' * 30)
4644 logging.error('\n' + '-' * 30 + '8<' + '-' * 30 + '\n\n\n')
4645 raise
4646 finally:
4647 # And then swap back to the original branch and clean up.
4648 RunGit(['checkout', '-q', cl.GetBranch()])
4649 RunGit(['branch', '-D', MERGE_BRANCH])
4650
4651 if not revision:
4652 print('Failed to push. If this persists, please file a bug.')
4653 return 1
4654
4655 killed = False
4656 if pushed_to_pending:
4657 try:
4658 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4659 # We set pushed_to_pending to False, since it made it all the way to the
4660 # real ref.
4661 pushed_to_pending = False
4662 except KeyboardInterrupt:
4663 killed = True
4664
4665 if cl.GetIssue():
4666 to_pending = ' to pending queue' if pushed_to_pending else ''
4667 viewvc_url = settings.GetViewVCUrl()
4668 if not to_pending:
4669 if viewvc_url and revision:
4670 change_desc.append_footer(
4671 'Committed: %s%s' % (viewvc_url, revision))
4672 elif revision:
4673 change_desc.append_footer('Committed: %s' % (revision,))
4674 print('Closing issue '
4675 '(you may be prompted for your codereview password)...')
4676 cl.UpdateDescription(change_desc.description)
4677 cl.CloseIssue()
4678 props = cl.GetIssueProperties()
4679 patch_num = len(props['patchsets'])
4680 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
4681 patch_num, props['patchsets'][-1], to_pending, revision)
4682 if options.bypass_hooks:
4683 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4684 else:
4685 comment += ' (presubmit successful).'
4686 cl.RpcServer().add_comment(cl.GetIssue(), comment)
4687
4688 if pushed_to_pending:
4689 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
4690 print('The commit is in the pending queue (%s).' % pending_ref)
4691 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4692 'footer.' % branch)
4693
4694 if os.path.isfile(POSTUPSTREAM_HOOK):
4695 RunCommand([POSTUPSTREAM_HOOK, merge_base], error_ok=True)
4696
4697 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004698
4699
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004700@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004701def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004702 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004703 parser.add_option('-b', dest='newbranch',
4704 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004705 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004706 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004707 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4708 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004709 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004710 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004711 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004712 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004713 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004714 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004715
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004716
4717 group = optparse.OptionGroup(
4718 parser,
4719 'Options for continuing work on the current issue uploaded from a '
4720 'different clone (e.g. different machine). Must be used independently '
4721 'from the other options. No issue number should be specified, and the '
4722 'branch must have an issue number associated with it')
4723 group.add_option('--reapply', action='store_true', dest='reapply',
4724 help='Reset the branch and reapply the issue.\n'
4725 'CAUTION: This will undo any local changes in this '
4726 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004727
4728 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004729 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004730 parser.add_option_group(group)
4731
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004732 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004733 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004734 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004735 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004736 auth_config = auth.extract_auth_config_from_options(options)
4737
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004738
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004739 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004740 if options.newbranch:
4741 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004742 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004743 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004744
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004745 cl = Changelist(auth_config=auth_config,
4746 codereview=options.forced_codereview)
4747 if not cl.GetIssue():
4748 parser.error('current branch must have an associated issue')
4749
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004750 upstream = cl.GetUpstreamBranch()
4751 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004752 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004753
4754 RunGit(['reset', '--hard', upstream])
4755 if options.pull:
4756 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004757
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004758 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4759 options.directory)
4760
4761 if len(args) != 1 or not args[0]:
4762 parser.error('Must specify issue number or url')
4763
4764 # We don't want uncommitted changes mixed up with the patch.
4765 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004766 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004767
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004768 if options.newbranch:
4769 if options.force:
4770 RunGit(['branch', '-D', options.newbranch],
4771 stderr=subprocess2.PIPE, error_ok=True)
4772 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004773 elif not GetCurrentBranch():
4774 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004775
4776 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4777
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004778 if cl.IsGerrit():
4779 if options.reject:
4780 parser.error('--reject is not supported with Gerrit codereview.')
4781 if options.nocommit:
4782 parser.error('--nocommit is not supported with Gerrit codereview.')
4783 if options.directory:
4784 parser.error('--directory is not supported with Gerrit codereview.')
4785
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004786 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004787 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004788
4789
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004790def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004791 """Fetches the tree status and returns either 'open', 'closed',
4792 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004793 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004794 if url:
4795 status = urllib2.urlopen(url).read().lower()
4796 if status.find('closed') != -1 or status == '0':
4797 return 'closed'
4798 elif status.find('open') != -1 or status == '1':
4799 return 'open'
4800 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004801 return 'unset'
4802
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004803
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004804def GetTreeStatusReason():
4805 """Fetches the tree status from a json url and returns the message
4806 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004807 url = settings.GetTreeStatusUrl()
4808 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004809 connection = urllib2.urlopen(json_url)
4810 status = json.loads(connection.read())
4811 connection.close()
4812 return status['message']
4813
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004814
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004815def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004816 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004817 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004818 status = GetTreeStatus()
4819 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004820 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004821 return 2
4822
vapiera7fbd5a2016-06-16 09:17:49 -07004823 print('The tree is %s' % status)
4824 print()
4825 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004826 if status != 'open':
4827 return 1
4828 return 0
4829
4830
maruel@chromium.org15192402012-09-06 12:38:29 +00004831def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004832 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004833 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004834 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004835 '-b', '--bot', action='append',
4836 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4837 'times to specify multiple builders. ex: '
4838 '"-b win_rel -b win_layout". See '
4839 'the try server waterfall for the builders name and the tests '
4840 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004841 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004842 '-B', '--bucket', default='',
4843 help=('Buildbucket bucket to send the try requests.'))
4844 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004845 '-m', '--master', default='',
4846 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004847 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004848 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004849 help='Revision to use for the try job; default: the revision will '
4850 'be determined by the try recipe that builder runs, which usually '
4851 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004852 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004853 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004854 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004855 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004856 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004857 '--project',
4858 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004859 'in recipe to determine to which repository or directory to '
4860 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004861 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004862 '-p', '--property', dest='properties', action='append', default=[],
4863 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004864 'key2=value2 etc. The value will be treated as '
4865 'json if decodable, or as string otherwise. '
4866 'NOTE: using this may make your try job not usable for CQ, '
4867 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004868 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004869 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4870 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004871 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004872 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004873 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004874 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004875
machenbach@chromium.org45453142015-09-15 08:45:22 +00004876 # Make sure that all properties are prop=value pairs.
4877 bad_params = [x for x in options.properties if '=' not in x]
4878 if bad_params:
4879 parser.error('Got properties with missing "=": %s' % bad_params)
4880
maruel@chromium.org15192402012-09-06 12:38:29 +00004881 if args:
4882 parser.error('Unknown arguments: %s' % args)
4883
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004884 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004885 if not cl.GetIssue():
4886 parser.error('Need to upload first')
4887
tandriie113dfd2016-10-11 10:20:12 -07004888 error_message = cl.CannotTriggerTryJobReason()
4889 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004890 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004891
borenet6c0efe62016-10-19 08:13:29 -07004892 if options.bucket and options.master:
4893 parser.error('Only one of --bucket and --master may be used.')
4894
qyearsley1fdfcb62016-10-24 13:22:03 -07004895 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004896
qyearsleydd49f942016-10-28 11:57:22 -07004897 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4898 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004899 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004900 if options.verbose:
4901 print('git cl try with no bots now defaults to CQ Dry Run.')
4902 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004903
borenet6c0efe62016-10-19 08:13:29 -07004904 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004905 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004906 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004907 'of bot requires an initial job from a parent (usually a builder). '
4908 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004909 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004910 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004911
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004912 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004913 # TODO(tandrii): Checking local patchset against remote patchset is only
4914 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4915 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07004916 print('Warning: Codereview server has newer patchsets (%s) than most '
4917 'recent upload from local checkout (%s). Did a previous upload '
4918 'fail?\n'
4919 'By default, git cl try uses the latest patchset from '
4920 'codereview, continuing to use patchset %s.\n' %
4921 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07004922
tandrii568043b2016-10-11 07:49:18 -07004923 try:
borenet6c0efe62016-10-19 08:13:29 -07004924 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
4925 patchset)
tandrii568043b2016-10-11 07:49:18 -07004926 except BuildbucketResponseException as ex:
4927 print('ERROR: %s' % ex)
4928 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004929 return 0
4930
4931
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004932def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004933 """Prints info about try jobs associated with current CL."""
4934 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004935 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004936 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004937 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004938 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004939 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004940 '--color', action='store_true', default=setup_color.IS_TTY,
4941 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004942 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004943 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4944 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004945 group.add_option(
4946 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004947 parser.add_option_group(group)
4948 auth.add_auth_options(parser)
4949 options, args = parser.parse_args(args)
4950 if args:
4951 parser.error('Unrecognized args: %s' % ' '.join(args))
4952
4953 auth_config = auth.extract_auth_config_from_options(options)
4954 cl = Changelist(auth_config=auth_config)
4955 if not cl.GetIssue():
4956 parser.error('Need to upload first')
4957
tandrii221ab252016-10-06 08:12:04 -07004958 patchset = options.patchset
4959 if not patchset:
4960 patchset = cl.GetMostRecentPatchset()
4961 if not patchset:
4962 parser.error('Codereview doesn\'t know about issue %s. '
4963 'No access to issue or wrong issue number?\n'
4964 'Either upload first, or pass --patchset explicitely' %
4965 cl.GetIssue())
4966
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004967 # TODO(tandrii): Checking local patchset against remote patchset is only
4968 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4969 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07004970 print('Warning: Codereview server has newer patchsets (%s) than most '
4971 'recent upload from local checkout (%s). Did a previous upload '
4972 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07004973 'By default, git cl try-results uses the latest patchset from '
4974 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07004975 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004976 try:
tandrii221ab252016-10-06 08:12:04 -07004977 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004978 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004979 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004980 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004981 if options.json:
4982 write_try_results_json(options.json, jobs)
4983 else:
4984 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004985 return 0
4986
4987
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004988@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004989def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004990 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004991 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004992 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004993 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004994
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004995 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004996 if args:
4997 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004998 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004999 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005000 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005001 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005002
5003 # Clear configured merge-base, if there is one.
5004 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005005 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005006 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005007 return 0
5008
5009
thestig@chromium.org00858c82013-12-02 23:08:03 +00005010def CMDweb(parser, args):
5011 """Opens the current CL in the web browser."""
5012 _, args = parser.parse_args(args)
5013 if args:
5014 parser.error('Unrecognized args: %s' % ' '.join(args))
5015
5016 issue_url = Changelist().GetIssueURL()
5017 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005018 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005019 return 1
5020
5021 webbrowser.open(issue_url)
5022 return 0
5023
5024
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005025def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005026 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005027 parser.add_option('-d', '--dry-run', action='store_true',
5028 help='trigger in dry run mode')
5029 parser.add_option('-c', '--clear', action='store_true',
5030 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005031 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005032 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005033 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005034 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005035 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005036 if args:
5037 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005038 if options.dry_run and options.clear:
5039 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5040
iannuccie53c9352016-08-17 14:40:40 -07005041 cl = Changelist(auth_config=auth_config, issue=options.issue,
5042 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005043 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005044 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005045 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07005046 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005047 state = _CQState.DRY_RUN
5048 else:
5049 state = _CQState.COMMIT
5050 if not cl.GetIssue():
5051 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005052 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005053 return 0
5054
5055
groby@chromium.org411034a2013-02-26 15:12:01 +00005056def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005057 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005058 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005059 auth.add_auth_options(parser)
5060 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005061 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005062 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005063 if args:
5064 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005065 cl = Changelist(auth_config=auth_config, issue=options.issue,
5066 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005067 # Ensure there actually is an issue to close.
5068 cl.GetDescription()
5069 cl.CloseIssue()
5070 return 0
5071
5072
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005073def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005074 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005075 parser.add_option(
5076 '--stat',
5077 action='store_true',
5078 dest='stat',
5079 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005080 auth.add_auth_options(parser)
5081 options, args = parser.parse_args(args)
5082 auth_config = auth.extract_auth_config_from_options(options)
5083 if args:
5084 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005085
5086 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005087 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005088 # Staged changes would be committed along with the patch from last
5089 # upload, hence counted toward the "last upload" side in the final
5090 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005091 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005092 return 1
5093
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005094 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005095 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005096 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005097 if not issue:
5098 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005099 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005100 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005101
5102 # Create a new branch based on the merge-base
5103 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005104 # Clear cached branch in cl object, to avoid overwriting original CL branch
5105 # properties.
5106 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005107 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005108 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005109 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005110 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005111 return rtn
5112
wychen@chromium.org06928532015-02-03 02:11:29 +00005113 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005114 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005115 cmd = ['git', 'diff']
5116 if options.stat:
5117 cmd.append('--stat')
5118 cmd.extend([TMP_BRANCH, branch, '--'])
5119 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005120 finally:
5121 RunGit(['checkout', '-q', branch])
5122 RunGit(['branch', '-D', TMP_BRANCH])
5123
5124 return 0
5125
5126
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005127def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005128 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005129 parser.add_option(
5130 '--no-color',
5131 action='store_true',
5132 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005133 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005134 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005135 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005136
5137 author = RunGit(['config', 'user.email']).strip() or None
5138
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005139 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005140
5141 if args:
5142 if len(args) > 1:
5143 parser.error('Unknown args')
5144 base_branch = args[0]
5145 else:
5146 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005147 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005148
5149 change = cl.GetChange(base_branch, None)
5150 return owners_finder.OwnersFinder(
5151 [f.LocalPath() for f in
5152 cl.GetChange(base_branch, None).AffectedFiles()],
5153 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005154 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005155 disable_color=options.no_color).run()
5156
5157
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005158def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005159 """Generates a diff command."""
5160 # Generate diff for the current branch's changes.
5161 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
5162 upstream_commit, '--' ]
5163
5164 if args:
5165 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005166 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005167 diff_cmd.append(arg)
5168 else:
5169 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005170
5171 return diff_cmd
5172
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005173def MatchingFileType(file_name, extensions):
5174 """Returns true if the file name ends with one of the given extensions."""
5175 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005176
enne@chromium.org555cfe42014-01-29 18:21:39 +00005177@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005178def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005179 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07005180 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005181 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005182 parser.add_option('--full', action='store_true',
5183 help='Reformat the full content of all touched files')
5184 parser.add_option('--dry-run', action='store_true',
5185 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005186 parser.add_option('--python', action='store_true',
5187 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005188 parser.add_option('--diff', action='store_true',
5189 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005190 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005191
Daniel Chengc55eecf2016-12-30 03:11:02 -08005192 # Normalize any remaining args against the current path, so paths relative to
5193 # the current directory are still resolved as expected.
5194 args = [os.path.join(os.getcwd(), arg) for arg in args]
5195
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005196 # git diff generates paths against the root of the repository. Change
5197 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005198 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005199 if rel_base_path:
5200 os.chdir(rel_base_path)
5201
digit@chromium.org29e47272013-05-17 17:01:46 +00005202 # Grab the merge-base commit, i.e. the upstream commit of the current
5203 # branch when it was created or the last time it was rebased. This is
5204 # to cover the case where the user may have called "git fetch origin",
5205 # moving the origin branch to a newer commit, but hasn't rebased yet.
5206 upstream_commit = None
5207 cl = Changelist()
5208 upstream_branch = cl.GetUpstreamBranch()
5209 if upstream_branch:
5210 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5211 upstream_commit = upstream_commit.strip()
5212
5213 if not upstream_commit:
5214 DieWithError('Could not find base commit for this branch. '
5215 'Are you in detached state?')
5216
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005217 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5218 diff_output = RunGit(changed_files_cmd)
5219 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005220 # Filter out files deleted by this CL
5221 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005222
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005223 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5224 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5225 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005226 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005227
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005228 top_dir = os.path.normpath(
5229 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5230
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005231 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5232 # formatted. This is used to block during the presubmit.
5233 return_value = 0
5234
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005235 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005236 # Locate the clang-format binary in the checkout
5237 try:
5238 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005239 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005240 DieWithError(e)
5241
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005242 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005243 cmd = [clang_format_tool]
5244 if not opts.dry_run and not opts.diff:
5245 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005246 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005247 if opts.diff:
5248 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005249 else:
5250 env = os.environ.copy()
5251 env['PATH'] = str(os.path.dirname(clang_format_tool))
5252 try:
5253 script = clang_format.FindClangFormatScriptInChromiumTree(
5254 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005255 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005256 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005257
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005258 cmd = [sys.executable, script, '-p0']
5259 if not opts.dry_run and not opts.diff:
5260 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005261
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005262 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5263 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005264
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005265 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5266 if opts.diff:
5267 sys.stdout.write(stdout)
5268 if opts.dry_run and len(stdout) > 0:
5269 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005270
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005271 # Similar code to above, but using yapf on .py files rather than clang-format
5272 # on C/C++ files
5273 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005274 yapf_tool = gclient_utils.FindExecutable('yapf')
5275 if yapf_tool is None:
5276 DieWithError('yapf not found in PATH')
5277
5278 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005279 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005280 cmd = [yapf_tool]
5281 if not opts.dry_run and not opts.diff:
5282 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005283 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005284 if opts.diff:
5285 sys.stdout.write(stdout)
5286 else:
5287 # TODO(sbc): yapf --lines mode still has some issues.
5288 # https://github.com/google/yapf/issues/154
5289 DieWithError('--python currently only works with --full')
5290
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005291 # Dart's formatter does not have the nice property of only operating on
5292 # modified chunks, so hard code full.
5293 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005294 try:
5295 command = [dart_format.FindDartFmtToolInChromiumTree()]
5296 if not opts.dry_run and not opts.diff:
5297 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005298 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005299
ppi@chromium.org6593d932016-03-03 15:41:15 +00005300 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005301 if opts.dry_run and stdout:
5302 return_value = 2
5303 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005304 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5305 'found in this checkout. Files in other languages are still '
5306 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005307
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005308 # Format GN build files. Always run on full build files for canonical form.
5309 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005310 cmd = ['gn', 'format' ]
5311 if opts.dry_run or opts.diff:
5312 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005313 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005314 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5315 shell=sys.platform == 'win32',
5316 cwd=top_dir)
5317 if opts.dry_run and gn_ret == 2:
5318 return_value = 2 # Not formatted.
5319 elif opts.diff and gn_ret == 2:
5320 # TODO this should compute and print the actual diff.
5321 print("This change has GN build file diff for " + gn_diff_file)
5322 elif gn_ret != 0:
5323 # For non-dry run cases (and non-2 return values for dry-run), a
5324 # nonzero error code indicates a failure, probably because the file
5325 # doesn't parse.
5326 DieWithError("gn format failed on " + gn_diff_file +
5327 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005328
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005329 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005330
5331
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005332@subcommand.usage('<codereview url or issue id>')
5333def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005334 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005335 _, args = parser.parse_args(args)
5336
5337 if len(args) != 1:
5338 parser.print_help()
5339 return 1
5340
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005341 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005342 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005343 parser.print_help()
5344 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005345 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005346
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005347 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005348 output = RunGit(['config', '--local', '--get-regexp',
5349 r'branch\..*\.%s' % issueprefix],
5350 error_ok=True)
5351 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005352 if issue == target_issue:
5353 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005354
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005355 branches = []
5356 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005357 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005358 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005359 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005360 return 1
5361 if len(branches) == 1:
5362 RunGit(['checkout', branches[0]])
5363 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005364 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005365 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005366 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005367 which = raw_input('Choose by index: ')
5368 try:
5369 RunGit(['checkout', branches[int(which)]])
5370 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005371 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005372 return 1
5373
5374 return 0
5375
5376
maruel@chromium.org29404b52014-09-08 22:58:00 +00005377def CMDlol(parser, args):
5378 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005379 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005380 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5381 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5382 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005383 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005384 return 0
5385
5386
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005387class OptionParser(optparse.OptionParser):
5388 """Creates the option parse and add --verbose support."""
5389 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005390 optparse.OptionParser.__init__(
5391 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005392 self.add_option(
5393 '-v', '--verbose', action='count', default=0,
5394 help='Use 2 times for more debugging info')
5395
5396 def parse_args(self, args=None, values=None):
5397 options, args = optparse.OptionParser.parse_args(self, args, values)
5398 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5399 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5400 return options, args
5401
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005402
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005403def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005404 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005405 print('\nYour python version %s is unsupported, please upgrade.\n' %
5406 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005407 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005408
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005409 # Reload settings.
5410 global settings
5411 settings = Settings()
5412
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005413 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005414 dispatcher = subcommand.CommandDispatcher(__name__)
5415 try:
5416 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005417 except auth.AuthenticationError as e:
5418 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005419 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005420 if e.code != 500:
5421 raise
5422 DieWithError(
5423 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5424 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005425 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005426
5427
5428if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005429 # These affect sys.stdout so do it outside of main() to simplify mocks in
5430 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005431 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005432 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005433 try:
5434 sys.exit(main(sys.argv[1:]))
5435 except KeyboardInterrupt:
5436 sys.stderr.write('interrupted\n')
5437 sys.exit(1)