blob: f11cee849d7c91fc663ab9e871b272d81e3f798d [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
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000888 def GetPendingRefPrefix(self):
889 if not self.pending_ref_prefix:
890 self.pending_ref_prefix = self._GetRietveldConfig(
891 'pending-ref-prefix', error_ok=True)
892 return self.pending_ref_prefix
893
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000894 def _GetRietveldConfig(self, param, **kwargs):
895 return self._GetConfig('rietveld.' + param, **kwargs)
896
rmistry@google.com78948ed2015-07-08 23:09:57 +0000897 def _GetBranchConfig(self, branch_name, param, **kwargs):
898 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
899
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000900 def _GetConfig(self, param, **kwargs):
901 self.LazyUpdateIfNeeded()
902 return RunGit(['config', param], **kwargs).strip()
903
904
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100905class _GitNumbererState(object):
906 KNOWN_PROJECTS_WHITELIST = [
907 'chromium/src',
908 'external/webrtc',
909 'v8/v8',
910 ]
911
912 @classmethod
913 def load(cls, remote_url, remote_ref):
914 """Figures out the state by fetching special refs from remote repo.
915 """
916 assert remote_ref and remote_ref.startswith('refs/'), remote_ref
917 url_parts = urlparse.urlparse(remote_url)
918 project_name = url_parts.path.lstrip('/').rstrip('git./')
919 for known in cls.KNOWN_PROJECTS_WHITELIST:
920 if project_name.endswith(known):
921 break
922 else:
923 # Early exit to avoid extra fetches for repos that aren't using gnumbd.
924 return cls(cls._get_pending_prefix_fallback(), None)
925
Quinten Yearsley442fb642016-12-15 15:38:27 -0800926 # This pollutes local ref space, but the amount of objects is negligible.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100927 error, _ = cls._run_git_with_code([
928 'fetch', remote_url,
929 '+refs/meta/config:refs/git_cl/meta/config',
930 '+refs/gnumbd-config/main:refs/git_cl/gnumbd-config/main'])
931 if error:
932 # Some ref doesn't exist or isn't accessible to current user.
933 # This shouldn't happen on production KNOWN_PROJECTS_WHITELIST
934 # with git-numberer.
935 cls._warn('failed to fetch gnumbd and project config for %s: %s',
936 remote_url, error)
937 return cls(cls._get_pending_prefix_fallback(), None)
938 return cls(cls._get_pending_prefix(remote_ref),
939 cls._is_validator_enabled(remote_ref))
940
941 @classmethod
942 def _get_pending_prefix(cls, ref):
943 error, gnumbd_config_data = cls._run_git_with_code(
944 ['show', 'refs/git_cl/gnumbd-config/main:config.json'])
945 if error:
946 cls._warn('gnumbd config file not found')
947 return cls._get_pending_prefix_fallback()
948
949 try:
950 config = json.loads(gnumbd_config_data)
951 if cls.match_refglobs(ref, config['enabled_refglobs']):
952 return config['pending_ref_prefix']
953 return None
954 except KeyboardInterrupt:
955 raise
956 except Exception as e:
957 cls._warn('failed to parse gnumbd config: %s', e)
958 return cls._get_pending_prefix_fallback()
959
960 @staticmethod
961 def _get_pending_prefix_fallback():
962 global settings
963 if not settings:
964 settings = Settings()
965 return settings.GetPendingRefPrefix()
966
967 @classmethod
968 def _is_validator_enabled(cls, ref):
969 error, project_config_data = cls._run_git_with_code(
970 ['show', 'refs/git_cl/meta/config:project.config'])
971 if error:
972 cls._warn('project.config file not found')
973 return False
974 # Gerrit's project.config is really a git config file.
975 # So, parse it as such.
976 with tempfile.NamedTemporaryFile(prefix='git_cl_proj_config') as f:
977 f.write(project_config_data)
978 # Make sure OS sees this, but don't close the file just yet,
979 # as NamedTemporaryFile deletes it on closing.
980 f.flush()
981
982 def get_opts(x):
983 code, out = cls._run_git_with_code(
984 ['config', '-f', f.name, '--get-all',
985 'plugin.git-numberer.validate-%s-refglob' % x])
986 if code == 0:
987 return out.strip().splitlines()
988 return []
989 enabled, disabled = map(get_opts, ['enabled', 'disabled'])
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100990 logging.info('validator config enabled %s disabled %s refglobs for '
991 '(this ref: %s)', enabled, disabled, ref)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100992
993 if cls.match_refglobs(ref, disabled):
994 return False
995 return cls.match_refglobs(ref, enabled)
996
997 @staticmethod
998 def match_refglobs(ref, refglobs):
999 for refglob in refglobs:
1000 if ref == refglob or fnmatch.fnmatch(ref, refglob):
1001 return True
1002 return False
1003
1004 @staticmethod
1005 def _run_git_with_code(*args, **kwargs):
1006 # The only reason for this wrapper is easy porting of this code to CQ
1007 # codebase, which forked git_cl.py and checkouts.py long time ago.
1008 return RunGitWithCode(*args, **kwargs)
1009
1010 @staticmethod
1011 def _warn(msg, *args):
1012 if args:
1013 msg = msg % args
1014 print('WARNING: %s' % msg)
1015
1016 def __init__(self, pending_prefix, validator_enabled):
1017 # TODO(tandrii): remove pending_prefix after gnumbd is no more.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01001018 if pending_prefix:
1019 if not pending_prefix.endswith('/'):
1020 pending_prefix += '/'
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001021 self._pending_prefix = pending_prefix or None
1022 self._validator_enabled = validator_enabled or False
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01001023 logging.debug('_GitNumbererState(pending: %s, validator: %s)',
1024 self._pending_prefix, self._validator_enabled)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001025
1026 @property
1027 def pending_prefix(self):
1028 return self._pending_prefix
1029
1030 @property
Andrii Shyshkalov8f15f3e2016-12-14 15:43:49 +01001031 def should_add_git_number(self):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001032 return self._validator_enabled and self._pending_prefix is None
1033
1034
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001035def ShortBranchName(branch):
1036 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001037 return branch.replace('refs/heads/', '', 1)
1038
1039
1040def GetCurrentBranchRef():
1041 """Returns branch ref (e.g., refs/heads/master) or None."""
1042 return RunGit(['symbolic-ref', 'HEAD'],
1043 stderr=subprocess2.VOID, error_ok=True).strip() or None
1044
1045
1046def GetCurrentBranch():
1047 """Returns current branch or None.
1048
1049 For refs/heads/* branches, returns just last part. For others, full ref.
1050 """
1051 branchref = GetCurrentBranchRef()
1052 if branchref:
1053 return ShortBranchName(branchref)
1054 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001055
1056
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001057class _CQState(object):
1058 """Enum for states of CL with respect to Commit Queue."""
1059 NONE = 'none'
1060 DRY_RUN = 'dry_run'
1061 COMMIT = 'commit'
1062
1063 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1064
1065
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001066class _ParsedIssueNumberArgument(object):
1067 def __init__(self, issue=None, patchset=None, hostname=None):
1068 self.issue = issue
1069 self.patchset = patchset
1070 self.hostname = hostname
1071
1072 @property
1073 def valid(self):
1074 return self.issue is not None
1075
1076
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001077def ParseIssueNumberArgument(arg):
1078 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1079 fail_result = _ParsedIssueNumberArgument()
1080
1081 if arg.isdigit():
1082 return _ParsedIssueNumberArgument(issue=int(arg))
1083 if not arg.startswith('http'):
1084 return fail_result
1085 url = gclient_utils.UpgradeToHttps(arg)
1086 try:
1087 parsed_url = urlparse.urlparse(url)
1088 except ValueError:
1089 return fail_result
1090 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
1091 tmp = cls.ParseIssueURL(parsed_url)
1092 if tmp is not None:
1093 return tmp
1094 return fail_result
1095
1096
Aaron Gablea45ee112016-11-22 15:14:38 -08001097class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001098 def __init__(self, issue, url):
1099 self.issue = issue
1100 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001101 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001102
1103 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001104 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001105 self.issue, self.url)
1106
1107
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001108class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001109 """Changelist works with one changelist in local branch.
1110
1111 Supports two codereview backends: Rietveld or Gerrit, selected at object
1112 creation.
1113
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001114 Notes:
1115 * Not safe for concurrent multi-{thread,process} use.
1116 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001117 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001118 """
1119
1120 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1121 """Create a new ChangeList instance.
1122
1123 If issue is given, the codereview must be given too.
1124
1125 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1126 Otherwise, it's decided based on current configuration of the local branch,
1127 with default being 'rietveld' for backwards compatibility.
1128 See _load_codereview_impl for more details.
1129
1130 **kwargs will be passed directly to codereview implementation.
1131 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001132 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001133 global settings
1134 if not settings:
1135 # Happens when git_cl.py is used as a utility library.
1136 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001137
1138 if issue:
1139 assert codereview, 'codereview must be known, if issue is known'
1140
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001141 self.branchref = branchref
1142 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001143 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001144 self.branch = ShortBranchName(self.branchref)
1145 else:
1146 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001147 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001148 self.lookedup_issue = False
1149 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001150 self.has_description = False
1151 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001152 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001153 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001154 self.cc = None
1155 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001156 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001157
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001158 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001159 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001160 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001161 assert self._codereview_impl
1162 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001163
1164 def _load_codereview_impl(self, codereview=None, **kwargs):
1165 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001166 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1167 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1168 self._codereview = codereview
1169 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001170 return
1171
1172 # Automatic selection based on issue number set for a current branch.
1173 # Rietveld takes precedence over Gerrit.
1174 assert not self.issue
1175 # Whether we find issue or not, we are doing the lookup.
1176 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001177 if self.GetBranch():
1178 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1179 issue = _git_get_branch_config_value(
1180 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1181 if issue:
1182 self._codereview = codereview
1183 self._codereview_impl = cls(self, **kwargs)
1184 self.issue = int(issue)
1185 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001186
1187 # No issue is set for this branch, so decide based on repo-wide settings.
1188 return self._load_codereview_impl(
1189 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1190 **kwargs)
1191
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001192 def IsGerrit(self):
1193 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001194
1195 def GetCCList(self):
1196 """Return the users cc'd on this CL.
1197
agable92bec4f2016-08-24 09:27:27 -07001198 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001199 """
1200 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001201 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001202 more_cc = ','.join(self.watchers)
1203 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1204 return self.cc
1205
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001206 def GetCCListWithoutDefault(self):
1207 """Return the users cc'd on this CL excluding default ones."""
1208 if self.cc is None:
1209 self.cc = ','.join(self.watchers)
1210 return self.cc
1211
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001212 def SetWatchers(self, watchers):
1213 """Set the list of email addresses that should be cc'd based on the changed
1214 files in this CL.
1215 """
1216 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001217
1218 def GetBranch(self):
1219 """Returns the short branch name, e.g. 'master'."""
1220 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001221 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001222 if not branchref:
1223 return None
1224 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001225 self.branch = ShortBranchName(self.branchref)
1226 return self.branch
1227
1228 def GetBranchRef(self):
1229 """Returns the full branch name, e.g. 'refs/heads/master'."""
1230 self.GetBranch() # Poke the lazy loader.
1231 return self.branchref
1232
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001233 def ClearBranch(self):
1234 """Clears cached branch data of this object."""
1235 self.branch = self.branchref = None
1236
tandrii5d48c322016-08-18 16:19:37 -07001237 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1238 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1239 kwargs['branch'] = self.GetBranch()
1240 return _git_get_branch_config_value(key, default, **kwargs)
1241
1242 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1243 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1244 assert self.GetBranch(), (
1245 'this CL must have an associated branch to %sset %s%s' %
1246 ('un' if value is None else '',
1247 key,
1248 '' if value is None else ' to %r' % value))
1249 kwargs['branch'] = self.GetBranch()
1250 return _git_set_branch_config_value(key, value, **kwargs)
1251
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001252 @staticmethod
1253 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001254 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001255 e.g. 'origin', 'refs/heads/master'
1256 """
1257 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001258 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1259
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001260 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001261 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001262 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001263 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1264 error_ok=True).strip()
1265 if upstream_branch:
1266 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001267 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001268 # Else, try to guess the origin remote.
1269 remote_branches = RunGit(['branch', '-r']).split()
1270 if 'origin/master' in remote_branches:
1271 # Fall back on origin/master if it exits.
1272 remote = 'origin'
1273 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001274 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001275 DieWithError(
1276 'Unable to determine default branch to diff against.\n'
1277 'Either pass complete "git diff"-style arguments, like\n'
1278 ' git cl upload origin/master\n'
1279 'or verify this branch is set up to track another \n'
1280 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001281
1282 return remote, upstream_branch
1283
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001284 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001285 upstream_branch = self.GetUpstreamBranch()
1286 if not BranchExists(upstream_branch):
1287 DieWithError('The upstream for the current branch (%s) does not exist '
1288 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001289 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001290 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001291
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001292 def GetUpstreamBranch(self):
1293 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001294 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001295 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001296 upstream_branch = upstream_branch.replace('refs/heads/',
1297 'refs/remotes/%s/' % remote)
1298 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1299 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001300 self.upstream_branch = upstream_branch
1301 return self.upstream_branch
1302
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001303 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001304 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001305 remote, branch = None, self.GetBranch()
1306 seen_branches = set()
1307 while branch not in seen_branches:
1308 seen_branches.add(branch)
1309 remote, branch = self.FetchUpstreamTuple(branch)
1310 branch = ShortBranchName(branch)
1311 if remote != '.' or branch.startswith('refs/remotes'):
1312 break
1313 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001314 remotes = RunGit(['remote'], error_ok=True).split()
1315 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001316 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001317 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001318 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001319 logging.warn('Could not determine which remote this change is '
1320 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001321 else:
1322 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001323 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001324 branch = 'HEAD'
1325 if branch.startswith('refs/remotes'):
1326 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001327 elif branch.startswith('refs/branch-heads/'):
1328 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001329 else:
1330 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001331 return self._remote
1332
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001333 def GitSanityChecks(self, upstream_git_obj):
1334 """Checks git repo status and ensures diff is from local commits."""
1335
sbc@chromium.org79706062015-01-14 21:18:12 +00001336 if upstream_git_obj is None:
1337 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001338 print('ERROR: unable to determine current branch (detached HEAD?)',
1339 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001340 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001341 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001342 return False
1343
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001344 # Verify the commit we're diffing against is in our current branch.
1345 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1346 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1347 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001348 print('ERROR: %s is not in the current branch. You may need to rebase '
1349 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001350 return False
1351
1352 # List the commits inside the diff, and verify they are all local.
1353 commits_in_diff = RunGit(
1354 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1355 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1356 remote_branch = remote_branch.strip()
1357 if code != 0:
1358 _, remote_branch = self.GetRemoteBranch()
1359
1360 commits_in_remote = RunGit(
1361 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1362
1363 common_commits = set(commits_in_diff) & set(commits_in_remote)
1364 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001365 print('ERROR: Your diff contains %d commits already in %s.\n'
1366 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1367 'the diff. If you are using a custom git flow, you can override'
1368 ' the reference used for this check with "git config '
1369 'gitcl.remotebranch <git-ref>".' % (
1370 len(common_commits), remote_branch, upstream_git_obj),
1371 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001372 return False
1373 return True
1374
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001375 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001376 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001377
1378 Returns None if it is not set.
1379 """
tandrii5d48c322016-08-18 16:19:37 -07001380 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001381
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001382 def GetRemoteUrl(self):
1383 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1384
1385 Returns None if there is no remote.
1386 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001387 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001388 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1389
1390 # If URL is pointing to a local directory, it is probably a git cache.
1391 if os.path.isdir(url):
1392 url = RunGit(['config', 'remote.%s.url' % remote],
1393 error_ok=True,
1394 cwd=url).strip()
1395 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001396
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001397 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001398 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001399 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001400 self.issue = self._GitGetBranchConfigValue(
1401 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001402 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001403 return self.issue
1404
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001405 def GetIssueURL(self):
1406 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001407 issue = self.GetIssue()
1408 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001409 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001410 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001411
1412 def GetDescription(self, pretty=False):
1413 if not self.has_description:
1414 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001415 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001416 self.has_description = True
1417 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001418 # Set width to 72 columns + 2 space indent.
1419 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001420 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001421 lines = self.description.splitlines()
1422 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001423 return self.description
1424
1425 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001426 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001427 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001428 self.patchset = self._GitGetBranchConfigValue(
1429 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001430 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001431 return self.patchset
1432
1433 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001434 """Set this branch's patchset. If patchset=0, clears the patchset."""
1435 assert self.GetBranch()
1436 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001437 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001438 else:
1439 self.patchset = int(patchset)
1440 self._GitSetBranchConfigValue(
1441 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001442
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001443 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001444 """Set this branch's issue. If issue isn't given, clears the issue."""
1445 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001446 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001447 issue = int(issue)
1448 self._GitSetBranchConfigValue(
1449 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001450 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001451 codereview_server = self._codereview_impl.GetCodereviewServer()
1452 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001453 self._GitSetBranchConfigValue(
1454 self._codereview_impl.CodereviewServerConfigKey(),
1455 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001456 else:
tandrii5d48c322016-08-18 16:19:37 -07001457 # Reset all of these just to be clean.
1458 reset_suffixes = [
1459 'last-upload-hash',
1460 self._codereview_impl.IssueConfigKey(),
1461 self._codereview_impl.PatchsetConfigKey(),
1462 self._codereview_impl.CodereviewServerConfigKey(),
1463 ] + self._PostUnsetIssueProperties()
1464 for prop in reset_suffixes:
1465 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001466 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001467 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001468
dnjba1b0f32016-09-02 12:37:42 -07001469 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001470 if not self.GitSanityChecks(upstream_branch):
1471 DieWithError('\nGit sanity check failure')
1472
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001473 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001474 if not root:
1475 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001476 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001477
1478 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001479 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001480 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001481 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001482 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001483 except subprocess2.CalledProcessError:
1484 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001485 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001486 'This branch probably doesn\'t exist anymore. To reset the\n'
1487 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001488 ' git branch --set-upstream-to origin/master %s\n'
1489 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001490 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001491
maruel@chromium.org52424302012-08-29 15:14:30 +00001492 issue = self.GetIssue()
1493 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001494 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001495 description = self.GetDescription()
1496 else:
1497 # If the change was never uploaded, use the log messages of all commits
1498 # up to the branch point, as git cl upload will prefill the description
1499 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001500 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1501 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001502
1503 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001504 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001505 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001506 name,
1507 description,
1508 absroot,
1509 files,
1510 issue,
1511 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001512 author,
1513 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001514
dsansomee2d6fd92016-09-08 00:10:47 -07001515 def UpdateDescription(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001516 self.description = description
dsansomee2d6fd92016-09-08 00:10:47 -07001517 return self._codereview_impl.UpdateDescriptionRemote(
1518 description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001519
1520 def RunHook(self, committing, may_prompt, verbose, change):
1521 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1522 try:
1523 return presubmit_support.DoPresubmitChecks(change, committing,
1524 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1525 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001526 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1527 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001528 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001529 DieWithError(
1530 ('%s\nMaybe your depot_tools is out of date?\n'
1531 'If all fails, contact maruel@') % e)
1532
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001533 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1534 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001535 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1536 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001537 else:
1538 # Assume url.
1539 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1540 urlparse.urlparse(issue_arg))
1541 if not parsed_issue_arg or not parsed_issue_arg.valid:
1542 DieWithError('Failed to parse issue argument "%s". '
1543 'Must be an issue number or a valid URL.' % issue_arg)
1544 return self._codereview_impl.CMDPatchWithParsedIssue(
1545 parsed_issue_arg, reject, nocommit, directory)
1546
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001547 def CMDUpload(self, options, git_diff_args, orig_args):
1548 """Uploads a change to codereview."""
1549 if git_diff_args:
1550 # TODO(ukai): is it ok for gerrit case?
1551 base_branch = git_diff_args[0]
1552 else:
1553 if self.GetBranch() is None:
1554 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1555
1556 # Default to diffing against common ancestor of upstream branch
1557 base_branch = self.GetCommonAncestorWithUpstream()
1558 git_diff_args = [base_branch, 'HEAD']
1559
1560 # Make sure authenticated to codereview before running potentially expensive
1561 # hooks. It is a fast, best efforts check. Codereview still can reject the
1562 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001563 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001564
1565 # Apply watchlists on upload.
1566 change = self.GetChange(base_branch, None)
1567 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1568 files = [f.LocalPath() for f in change.AffectedFiles()]
1569 if not options.bypass_watchlists:
1570 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1571
1572 if not options.bypass_hooks:
1573 if options.reviewers or options.tbr_owners:
1574 # Set the reviewer list now so that presubmit checks can access it.
1575 change_description = ChangeDescription(change.FullDescriptionText())
1576 change_description.update_reviewers(options.reviewers,
1577 options.tbr_owners,
1578 change)
1579 change.SetDescriptionText(change_description.description)
1580 hook_results = self.RunHook(committing=False,
1581 may_prompt=not options.force,
1582 verbose=options.verbose,
1583 change=change)
1584 if not hook_results.should_continue():
1585 return 1
1586 if not options.reviewers and hook_results.reviewers:
1587 options.reviewers = hook_results.reviewers.split(',')
1588
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001589 # TODO(tandrii): Checking local patchset against remote patchset is only
1590 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1591 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001592 latest_patchset = self.GetMostRecentPatchset()
1593 local_patchset = self.GetPatchset()
1594 if (latest_patchset and local_patchset and
1595 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001596 print('The last upload made from this repository was patchset #%d but '
1597 'the most recent patchset on the server is #%d.'
1598 % (local_patchset, latest_patchset))
1599 print('Uploading will still work, but if you\'ve uploaded to this '
1600 'issue from another machine or branch the patch you\'re '
1601 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001602 ask_for_data('About to upload; enter to confirm.')
1603
1604 print_stats(options.similarity, options.find_copies, git_diff_args)
1605 ret = self.CMDUploadChange(options, git_diff_args, change)
1606 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001607 if options.use_commit_queue:
1608 self.SetCQState(_CQState.COMMIT)
1609 elif options.cq_dry_run:
1610 self.SetCQState(_CQState.DRY_RUN)
1611
tandrii5d48c322016-08-18 16:19:37 -07001612 _git_set_branch_config_value('last-upload-hash',
1613 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001614 # Run post upload hooks, if specified.
1615 if settings.GetRunPostUploadHook():
1616 presubmit_support.DoPostUploadExecuter(
1617 change,
1618 self,
1619 settings.GetRoot(),
1620 options.verbose,
1621 sys.stdout)
1622
1623 # Upload all dependencies if specified.
1624 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001625 print()
1626 print('--dependencies has been specified.')
1627 print('All dependent local branches will be re-uploaded.')
1628 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001629 # Remove the dependencies flag from args so that we do not end up in a
1630 # loop.
1631 orig_args.remove('--dependencies')
1632 ret = upload_branch_deps(self, orig_args)
1633 return ret
1634
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001635 def SetCQState(self, new_state):
1636 """Update the CQ state for latest patchset.
1637
1638 Issue must have been already uploaded and known.
1639 """
1640 assert new_state in _CQState.ALL_STATES
1641 assert self.GetIssue()
1642 return self._codereview_impl.SetCQState(new_state)
1643
qyearsley1fdfcb62016-10-24 13:22:03 -07001644 def TriggerDryRun(self):
1645 """Triggers a dry run and prints a warning on failure."""
1646 # TODO(qyearsley): Either re-use this method in CMDset_commit
1647 # and CMDupload, or change CMDtry to trigger dry runs with
1648 # just SetCQState, and catch keyboard interrupt and other
1649 # errors in that method.
1650 try:
1651 self.SetCQState(_CQState.DRY_RUN)
1652 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1653 return 0
1654 except KeyboardInterrupt:
1655 raise
1656 except:
1657 print('WARNING: failed to trigger CQ Dry Run.\n'
1658 'Either:\n'
1659 ' * your project has no CQ\n'
1660 ' * you don\'t have permission to trigger Dry Run\n'
1661 ' * bug in this code (see stack trace below).\n'
1662 'Consider specifying which bots to trigger manually '
1663 'or asking your project owners for permissions '
1664 'or contacting Chrome Infrastructure team at '
1665 'https://www.chromium.org/infra\n\n')
1666 # Still raise exception so that stack trace is printed.
1667 raise
1668
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001669 # Forward methods to codereview specific implementation.
1670
1671 def CloseIssue(self):
1672 return self._codereview_impl.CloseIssue()
1673
1674 def GetStatus(self):
1675 return self._codereview_impl.GetStatus()
1676
1677 def GetCodereviewServer(self):
1678 return self._codereview_impl.GetCodereviewServer()
1679
tandriide281ae2016-10-12 06:02:30 -07001680 def GetIssueOwner(self):
1681 """Get owner from codereview, which may differ from this checkout."""
1682 return self._codereview_impl.GetIssueOwner()
1683
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001684 def GetApprovingReviewers(self):
1685 return self._codereview_impl.GetApprovingReviewers()
1686
1687 def GetMostRecentPatchset(self):
1688 return self._codereview_impl.GetMostRecentPatchset()
1689
tandriide281ae2016-10-12 06:02:30 -07001690 def CannotTriggerTryJobReason(self):
1691 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1692 return self._codereview_impl.CannotTriggerTryJobReason()
1693
tandrii8c5a3532016-11-04 07:52:02 -07001694 def GetTryjobProperties(self, patchset=None):
1695 """Returns dictionary of properties to launch tryjob."""
1696 return self._codereview_impl.GetTryjobProperties(patchset=patchset)
1697
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001698 def __getattr__(self, attr):
1699 # This is because lots of untested code accesses Rietveld-specific stuff
1700 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001701 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001702 # Note that child method defines __getattr__ as well, and forwards it here,
1703 # because _RietveldChangelistImpl is not cleaned up yet, and given
1704 # deprecation of Rietveld, it should probably be just removed.
1705 # Until that time, avoid infinite recursion by bypassing __getattr__
1706 # of implementation class.
1707 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001708
1709
1710class _ChangelistCodereviewBase(object):
1711 """Abstract base class encapsulating codereview specifics of a changelist."""
1712 def __init__(self, changelist):
1713 self._changelist = changelist # instance of Changelist
1714
1715 def __getattr__(self, attr):
1716 # Forward methods to changelist.
1717 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1718 # _RietveldChangelistImpl to avoid this hack?
1719 return getattr(self._changelist, attr)
1720
1721 def GetStatus(self):
1722 """Apply a rough heuristic to give a simple summary of an issue's review
1723 or CQ status, assuming adherence to a common workflow.
1724
1725 Returns None if no issue for this branch, or specific string keywords.
1726 """
1727 raise NotImplementedError()
1728
1729 def GetCodereviewServer(self):
1730 """Returns server URL without end slash, like "https://codereview.com"."""
1731 raise NotImplementedError()
1732
1733 def FetchDescription(self):
1734 """Fetches and returns description from the codereview server."""
1735 raise NotImplementedError()
1736
tandrii5d48c322016-08-18 16:19:37 -07001737 @classmethod
1738 def IssueConfigKey(cls):
1739 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001740 raise NotImplementedError()
1741
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001742 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001743 def PatchsetConfigKey(cls):
1744 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001745 raise NotImplementedError()
1746
tandrii5d48c322016-08-18 16:19:37 -07001747 @classmethod
1748 def CodereviewServerConfigKey(cls):
1749 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001750 raise NotImplementedError()
1751
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001752 def _PostUnsetIssueProperties(self):
1753 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001754 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001755
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001756 def GetRieveldObjForPresubmit(self):
1757 # This is an unfortunate Rietveld-embeddedness in presubmit.
1758 # For non-Rietveld codereviews, this probably should return a dummy object.
1759 raise NotImplementedError()
1760
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001761 def GetGerritObjForPresubmit(self):
1762 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1763 return None
1764
dsansomee2d6fd92016-09-08 00:10:47 -07001765 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001766 """Update the description on codereview site."""
1767 raise NotImplementedError()
1768
1769 def CloseIssue(self):
1770 """Closes the issue."""
1771 raise NotImplementedError()
1772
1773 def GetApprovingReviewers(self):
1774 """Returns a list of reviewers approving the change.
1775
1776 Note: not necessarily committers.
1777 """
1778 raise NotImplementedError()
1779
1780 def GetMostRecentPatchset(self):
1781 """Returns the most recent patchset number from the codereview site."""
1782 raise NotImplementedError()
1783
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001784 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1785 directory):
1786 """Fetches and applies the issue.
1787
1788 Arguments:
1789 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1790 reject: if True, reject the failed patch instead of switching to 3-way
1791 merge. Rietveld only.
1792 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1793 only.
1794 directory: switch to directory before applying the patch. Rietveld only.
1795 """
1796 raise NotImplementedError()
1797
1798 @staticmethod
1799 def ParseIssueURL(parsed_url):
1800 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1801 failed."""
1802 raise NotImplementedError()
1803
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001804 def EnsureAuthenticated(self, force):
1805 """Best effort check that user is authenticated with codereview server.
1806
1807 Arguments:
1808 force: whether to skip confirmation questions.
1809 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001810 raise NotImplementedError()
1811
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001812 def CMDUploadChange(self, options, args, change):
1813 """Uploads a change to codereview."""
1814 raise NotImplementedError()
1815
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001816 def SetCQState(self, new_state):
1817 """Update the CQ state for latest patchset.
1818
1819 Issue must have been already uploaded and known.
1820 """
1821 raise NotImplementedError()
1822
tandriie113dfd2016-10-11 10:20:12 -07001823 def CannotTriggerTryJobReason(self):
1824 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1825 raise NotImplementedError()
1826
tandriide281ae2016-10-12 06:02:30 -07001827 def GetIssueOwner(self):
1828 raise NotImplementedError()
1829
tandrii8c5a3532016-11-04 07:52:02 -07001830 def GetTryjobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001831 raise NotImplementedError()
1832
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001833
1834class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1835 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1836 super(_RietveldChangelistImpl, self).__init__(changelist)
1837 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001838 if not rietveld_server:
1839 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001840
1841 self._rietveld_server = rietveld_server
1842 self._auth_config = auth_config
1843 self._props = None
1844 self._rpc_server = None
1845
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001846 def GetCodereviewServer(self):
1847 if not self._rietveld_server:
1848 # If we're on a branch then get the server potentially associated
1849 # with that branch.
1850 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001851 self._rietveld_server = gclient_utils.UpgradeToHttps(
1852 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001853 if not self._rietveld_server:
1854 self._rietveld_server = settings.GetDefaultServerUrl()
1855 return self._rietveld_server
1856
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001857 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001858 """Best effort check that user is authenticated with Rietveld server."""
1859 if self._auth_config.use_oauth2:
1860 authenticator = auth.get_authenticator_for_host(
1861 self.GetCodereviewServer(), self._auth_config)
1862 if not authenticator.has_cached_credentials():
1863 raise auth.LoginRequiredError(self.GetCodereviewServer())
1864
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001865 def FetchDescription(self):
1866 issue = self.GetIssue()
1867 assert issue
1868 try:
1869 return self.RpcServer().get_description(issue).strip()
1870 except urllib2.HTTPError as e:
1871 if e.code == 404:
1872 DieWithError(
1873 ('\nWhile fetching the description for issue %d, received a '
1874 '404 (not found)\n'
1875 'error. It is likely that you deleted this '
1876 'issue on the server. If this is the\n'
1877 'case, please run\n\n'
1878 ' git cl issue 0\n\n'
1879 'to clear the association with the deleted issue. Then run '
1880 'this command again.') % issue)
1881 else:
1882 DieWithError(
1883 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1884 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001885 print('Warning: Failed to retrieve CL description due to network '
1886 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001887 return ''
1888
1889 def GetMostRecentPatchset(self):
1890 return self.GetIssueProperties()['patchsets'][-1]
1891
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001892 def GetIssueProperties(self):
1893 if self._props is None:
1894 issue = self.GetIssue()
1895 if not issue:
1896 self._props = {}
1897 else:
1898 self._props = self.RpcServer().get_issue_properties(issue, True)
1899 return self._props
1900
tandriie113dfd2016-10-11 10:20:12 -07001901 def CannotTriggerTryJobReason(self):
1902 props = self.GetIssueProperties()
1903 if not props:
1904 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1905 if props.get('closed'):
1906 return 'CL %s is closed' % self.GetIssue()
1907 if props.get('private'):
1908 return 'CL %s is private' % self.GetIssue()
1909 return None
1910
tandrii8c5a3532016-11-04 07:52:02 -07001911 def GetTryjobProperties(self, patchset=None):
1912 """Returns dictionary of properties to launch tryjob."""
1913 project = (self.GetIssueProperties() or {}).get('project')
1914 return {
1915 'issue': self.GetIssue(),
1916 'patch_project': project,
1917 'patch_storage': 'rietveld',
1918 'patchset': patchset or self.GetPatchset(),
1919 'rietveld': self.GetCodereviewServer(),
1920 }
1921
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001922 def GetApprovingReviewers(self):
1923 return get_approving_reviewers(self.GetIssueProperties())
1924
tandriide281ae2016-10-12 06:02:30 -07001925 def GetIssueOwner(self):
1926 return (self.GetIssueProperties() or {}).get('owner_email')
1927
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001928 def AddComment(self, message):
1929 return self.RpcServer().add_comment(self.GetIssue(), message)
1930
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001931 def GetStatus(self):
1932 """Apply a rough heuristic to give a simple summary of an issue's review
1933 or CQ status, assuming adherence to a common workflow.
1934
1935 Returns None if no issue for this branch, or one of the following keywords:
1936 * 'error' - error from review tool (including deleted issues)
1937 * 'unsent' - not sent for review
1938 * 'waiting' - waiting for review
1939 * 'reply' - waiting for owner to reply to review
1940 * 'lgtm' - LGTM from at least one approved reviewer
1941 * 'commit' - in the commit queue
1942 * 'closed' - closed
1943 """
1944 if not self.GetIssue():
1945 return None
1946
1947 try:
1948 props = self.GetIssueProperties()
1949 except urllib2.HTTPError:
1950 return 'error'
1951
1952 if props.get('closed'):
1953 # Issue is closed.
1954 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001955 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001956 # Issue is in the commit queue.
1957 return 'commit'
1958
1959 try:
1960 reviewers = self.GetApprovingReviewers()
1961 except urllib2.HTTPError:
1962 return 'error'
1963
1964 if reviewers:
1965 # Was LGTM'ed.
1966 return 'lgtm'
1967
1968 messages = props.get('messages') or []
1969
tandrii9d2c7a32016-06-22 03:42:45 -07001970 # Skip CQ messages that don't require owner's action.
1971 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1972 if 'Dry run:' in messages[-1]['text']:
1973 messages.pop()
1974 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1975 # This message always follows prior messages from CQ,
1976 # so skip this too.
1977 messages.pop()
1978 else:
1979 # This is probably a CQ messages warranting user attention.
1980 break
1981
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001982 if not messages:
1983 # No message was sent.
1984 return 'unsent'
1985 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001986 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001987 return 'reply'
1988 return 'waiting'
1989
dsansomee2d6fd92016-09-08 00:10:47 -07001990 def UpdateDescriptionRemote(self, description, force=False):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001991 return self.RpcServer().update_description(
1992 self.GetIssue(), self.description)
1993
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001994 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001995 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001996
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001997 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07001998 return self.SetFlags({flag: value})
1999
2000 def SetFlags(self, flags):
2001 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002002 """
phajdan.jr68598232016-08-10 03:28:28 -07002003 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002004 try:
tandrii4b233bd2016-07-06 03:50:29 -07002005 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002006 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002007 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002008 if e.code == 404:
2009 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2010 if e.code == 403:
2011 DieWithError(
2012 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002013 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002014 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002015
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002016 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002017 """Returns an upload.RpcServer() to access this review's rietveld instance.
2018 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002019 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002020 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002021 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002022 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002023 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002024
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002025 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002026 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002027 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002028
tandrii5d48c322016-08-18 16:19:37 -07002029 @classmethod
2030 def PatchsetConfigKey(cls):
2031 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002032
tandrii5d48c322016-08-18 16:19:37 -07002033 @classmethod
2034 def CodereviewServerConfigKey(cls):
2035 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002036
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002037 def GetRieveldObjForPresubmit(self):
2038 return self.RpcServer()
2039
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002040 def SetCQState(self, new_state):
2041 props = self.GetIssueProperties()
2042 if props.get('private'):
2043 DieWithError('Cannot set-commit on private issue')
2044
2045 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002046 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002047 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002048 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002049 else:
tandrii4b233bd2016-07-06 03:50:29 -07002050 assert new_state == _CQState.DRY_RUN
2051 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002052
2053
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002054 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2055 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002056 # PatchIssue should never be called with a dirty tree. It is up to the
2057 # caller to check this, but just in case we assert here since the
2058 # consequences of the caller not checking this could be dire.
2059 assert(not git_common.is_dirty_git_tree('apply'))
2060 assert(parsed_issue_arg.valid)
2061 self._changelist.issue = parsed_issue_arg.issue
2062 if parsed_issue_arg.hostname:
2063 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2064
skobes6468b902016-10-24 08:45:10 -07002065 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2066 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2067 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002068 try:
skobes6468b902016-10-24 08:45:10 -07002069 scm_obj.apply_patch(patchset_object)
2070 except Exception as e:
2071 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002072 return 1
2073
2074 # If we had an issue, commit the current state and register the issue.
2075 if not nocommit:
2076 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2077 'patch from issue %(i)s at patchset '
2078 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2079 % {'i': self.GetIssue(), 'p': patchset})])
2080 self.SetIssue(self.GetIssue())
2081 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07002082 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002083 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002084 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002085 return 0
2086
2087 @staticmethod
2088 def ParseIssueURL(parsed_url):
2089 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2090 return None
wychen3c1c1722016-08-04 11:46:36 -07002091 # Rietveld patch: https://domain/<number>/#ps<patchset>
2092 match = re.match(r'/(\d+)/$', parsed_url.path)
2093 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2094 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002095 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002096 issue=int(match.group(1)),
2097 patchset=int(match2.group(1)),
2098 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002099 # Typical url: https://domain/<issue_number>[/[other]]
2100 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2101 if match:
skobes6468b902016-10-24 08:45:10 -07002102 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002103 issue=int(match.group(1)),
2104 hostname=parsed_url.netloc)
2105 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2106 match = re.match(r'/download/issue(\d+)_(\d+).diff$', 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 patchset=int(match.group(2)),
skobes6468b902016-10-24 08:45:10 -07002111 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002112 return None
2113
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002114 def CMDUploadChange(self, options, args, change):
2115 """Upload the patch to Rietveld."""
2116 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2117 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002118 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2119 if options.emulate_svn_auto_props:
2120 upload_args.append('--emulate_svn_auto_props')
2121
2122 change_desc = None
2123
2124 if options.email is not None:
2125 upload_args.extend(['--email', options.email])
2126
2127 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002128 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002129 upload_args.extend(['--title', options.title])
2130 if options.message:
2131 upload_args.extend(['--message', options.message])
2132 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002133 print('This branch is associated with issue %s. '
2134 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002135 else:
nodirca166002016-06-27 10:59:51 -07002136 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002137 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002138 if options.message:
2139 message = options.message
2140 else:
2141 message = CreateDescriptionFromLog(args)
2142 if options.title:
2143 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002144 change_desc = ChangeDescription(message)
2145 if options.reviewers or options.tbr_owners:
2146 change_desc.update_reviewers(options.reviewers,
2147 options.tbr_owners,
2148 change)
2149 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002150 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002151
2152 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002153 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002154 return 1
2155
2156 upload_args.extend(['--message', change_desc.description])
2157 if change_desc.get_reviewers():
2158 upload_args.append('--reviewers=%s' % ','.join(
2159 change_desc.get_reviewers()))
2160 if options.send_mail:
2161 if not change_desc.get_reviewers():
2162 DieWithError("Must specify reviewers to send email.")
2163 upload_args.append('--send_mail')
2164
2165 # We check this before applying rietveld.private assuming that in
2166 # rietveld.cc only addresses which we can send private CLs to are listed
2167 # if rietveld.private is set, and so we should ignore rietveld.cc only
2168 # when --private is specified explicitly on the command line.
2169 if options.private:
2170 logging.warn('rietveld.cc is ignored since private flag is specified. '
2171 'You need to review and add them manually if necessary.')
2172 cc = self.GetCCListWithoutDefault()
2173 else:
2174 cc = self.GetCCList()
2175 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002176 if change_desc.get_cced():
2177 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002178 if cc:
2179 upload_args.extend(['--cc', cc])
2180
2181 if options.private or settings.GetDefaultPrivateFlag() == "True":
2182 upload_args.append('--private')
2183
2184 upload_args.extend(['--git_similarity', str(options.similarity)])
2185 if not options.find_copies:
2186 upload_args.extend(['--git_no_find_copies'])
2187
2188 # Include the upstream repo's URL in the change -- this is useful for
2189 # projects that have their source spread across multiple repos.
2190 remote_url = self.GetGitBaseUrlFromConfig()
2191 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002192 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2193 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2194 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002195 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002196 remote, remote_branch = self.GetRemoteBranch()
2197 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01002198 pending_prefix_check=True,
2199 remote_url=self.GetRemoteUrl())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002200 if target_ref:
2201 upload_args.extend(['--target_ref', target_ref])
2202
2203 # Look for dependent patchsets. See crbug.com/480453 for more details.
2204 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2205 upstream_branch = ShortBranchName(upstream_branch)
2206 if remote is '.':
2207 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002208 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002209 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002210 print()
2211 print('Skipping dependency patchset upload because git config '
2212 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2213 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002214 else:
2215 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002216 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002217 auth_config=auth_config)
2218 branch_cl_issue_url = branch_cl.GetIssueURL()
2219 branch_cl_issue = branch_cl.GetIssue()
2220 branch_cl_patchset = branch_cl.GetPatchset()
2221 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2222 upload_args.extend(
2223 ['--depends_on_patchset', '%s:%s' % (
2224 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002225 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002226 '\n'
2227 'The current branch (%s) is tracking a local branch (%s) with '
2228 'an associated CL.\n'
2229 'Adding %s/#ps%s as a dependency patchset.\n'
2230 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2231 branch_cl_patchset))
2232
2233 project = settings.GetProject()
2234 if project:
2235 upload_args.extend(['--project', project])
2236
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002237 try:
2238 upload_args = ['upload'] + upload_args + args
2239 logging.info('upload.RealMain(%s)', upload_args)
2240 issue, patchset = upload.RealMain(upload_args)
2241 issue = int(issue)
2242 patchset = int(patchset)
2243 except KeyboardInterrupt:
2244 sys.exit(1)
2245 except:
2246 # If we got an exception after the user typed a description for their
2247 # change, back up the description before re-raising.
2248 if change_desc:
2249 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2250 print('\nGot exception while uploading -- saving description to %s\n' %
2251 backup_path)
2252 backup_file = open(backup_path, 'w')
2253 backup_file.write(change_desc.description)
2254 backup_file.close()
2255 raise
2256
2257 if not self.GetIssue():
2258 self.SetIssue(issue)
2259 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002260 return 0
2261
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002262
2263class _GerritChangelistImpl(_ChangelistCodereviewBase):
2264 def __init__(self, changelist, auth_config=None):
2265 # auth_config is Rietveld thing, kept here to preserve interface only.
2266 super(_GerritChangelistImpl, self).__init__(changelist)
2267 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002268 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002269 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002270 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002271
2272 def _GetGerritHost(self):
2273 # Lazy load of configs.
2274 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002275 if self._gerrit_host and '.' not in self._gerrit_host:
2276 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2277 # This happens for internal stuff http://crbug.com/614312.
2278 parsed = urlparse.urlparse(self.GetRemoteUrl())
2279 if parsed.scheme == 'sso':
2280 print('WARNING: using non https URLs for remote is likely broken\n'
2281 ' Your current remote is: %s' % self.GetRemoteUrl())
2282 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2283 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002284 return self._gerrit_host
2285
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002286 def _GetGitHost(self):
2287 """Returns git host to be used when uploading change to Gerrit."""
2288 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2289
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002290 def GetCodereviewServer(self):
2291 if not self._gerrit_server:
2292 # If we're on a branch then get the server potentially associated
2293 # with that branch.
2294 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002295 self._gerrit_server = self._GitGetBranchConfigValue(
2296 self.CodereviewServerConfigKey())
2297 if self._gerrit_server:
2298 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002299 if not self._gerrit_server:
2300 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2301 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002302 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002303 parts[0] = parts[0] + '-review'
2304 self._gerrit_host = '.'.join(parts)
2305 self._gerrit_server = 'https://%s' % self._gerrit_host
2306 return self._gerrit_server
2307
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002308 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002309 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002310 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002311
tandrii5d48c322016-08-18 16:19:37 -07002312 @classmethod
2313 def PatchsetConfigKey(cls):
2314 return 'gerritpatchset'
2315
2316 @classmethod
2317 def CodereviewServerConfigKey(cls):
2318 return 'gerritserver'
2319
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002320 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002321 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002322 if settings.GetGerritSkipEnsureAuthenticated():
2323 # For projects with unusual authentication schemes.
2324 # See http://crbug.com/603378.
2325 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002326 # Lazy-loader to identify Gerrit and Git hosts.
2327 if gerrit_util.GceAuthenticator.is_gce():
2328 return
2329 self.GetCodereviewServer()
2330 git_host = self._GetGitHost()
2331 assert self._gerrit_server and self._gerrit_host
2332 cookie_auth = gerrit_util.CookiesAuthenticator()
2333
2334 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2335 git_auth = cookie_auth.get_auth_header(git_host)
2336 if gerrit_auth and git_auth:
2337 if gerrit_auth == git_auth:
2338 return
2339 print((
2340 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2341 ' Check your %s or %s file for credentials of hosts:\n'
2342 ' %s\n'
2343 ' %s\n'
2344 ' %s') %
2345 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2346 git_host, self._gerrit_host,
2347 cookie_auth.get_new_password_message(git_host)))
2348 if not force:
2349 ask_for_data('If you know what you are doing, press Enter to continue, '
2350 'Ctrl+C to abort.')
2351 return
2352 else:
2353 missing = (
2354 [] if gerrit_auth else [self._gerrit_host] +
2355 [] if git_auth else [git_host])
2356 DieWithError('Credentials for the following hosts are required:\n'
2357 ' %s\n'
2358 'These are read from %s (or legacy %s)\n'
2359 '%s' % (
2360 '\n '.join(missing),
2361 cookie_auth.get_gitcookies_path(),
2362 cookie_auth.get_netrc_path(),
2363 cookie_auth.get_new_password_message(git_host)))
2364
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002365 def _PostUnsetIssueProperties(self):
2366 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002367 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002368
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002369 def GetRieveldObjForPresubmit(self):
2370 class ThisIsNotRietveldIssue(object):
2371 def __nonzero__(self):
2372 # This is a hack to make presubmit_support think that rietveld is not
2373 # defined, yet still ensure that calls directly result in a decent
2374 # exception message below.
2375 return False
2376
2377 def __getattr__(self, attr):
2378 print(
2379 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2380 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2381 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2382 'or use Rietveld for codereview.\n'
2383 'See also http://crbug.com/579160.' % attr)
2384 raise NotImplementedError()
2385 return ThisIsNotRietveldIssue()
2386
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002387 def GetGerritObjForPresubmit(self):
2388 return presubmit_support.GerritAccessor(self._GetGerritHost())
2389
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002390 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002391 """Apply a rough heuristic to give a simple summary of an issue's review
2392 or CQ status, assuming adherence to a common workflow.
2393
2394 Returns None if no issue for this branch, or one of the following keywords:
2395 * 'error' - error from review tool (including deleted issues)
2396 * 'unsent' - no reviewers added
2397 * 'waiting' - waiting for review
2398 * 'reply' - waiting for owner to reply to review
Quinten Yearsley442fb642016-12-15 15:38:27 -08002399 * 'not lgtm' - Code-Review disapproval from at least one valid reviewer
tandriic2405f52016-10-10 08:13:15 -07002400 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002401 * 'commit' - in the commit queue
2402 * 'closed' - abandoned
2403 """
2404 if not self.GetIssue():
2405 return None
2406
2407 try:
2408 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002409 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002410 return 'error'
2411
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002412 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002413 return 'closed'
2414
2415 cq_label = data['labels'].get('Commit-Queue', {})
2416 if cq_label:
rmistryc9ebbd22016-10-14 12:35:54 -07002417 votes = cq_label.get('all', [])
2418 highest_vote = 0
2419 for v in votes:
2420 highest_vote = max(highest_vote, v.get('value', 0))
2421 vote_value = str(highest_vote)
2422 if vote_value != '0':
2423 # Add a '+' if the value is not 0 to match the values in the label.
2424 # The cq_label does not have negatives.
2425 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002426 vote_text = cq_label.get('values', {}).get(vote_value, '')
2427 if vote_text.lower() == 'commit':
2428 return 'commit'
2429
2430 lgtm_label = data['labels'].get('Code-Review', {})
2431 if lgtm_label:
2432 if 'rejected' in lgtm_label:
2433 return 'not lgtm'
2434 if 'approved' in lgtm_label:
2435 return 'lgtm'
2436
2437 if not data.get('reviewers', {}).get('REVIEWER', []):
2438 return 'unsent'
2439
2440 messages = data.get('messages', [])
2441 if messages:
2442 owner = data['owner'].get('_account_id')
2443 last_message_author = messages[-1].get('author', {}).get('_account_id')
2444 if owner != last_message_author:
2445 # Some reply from non-owner.
2446 return 'reply'
2447
2448 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002449
2450 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002451 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002452 return data['revisions'][data['current_revision']]['_number']
2453
2454 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002455 data = self._GetChangeDetail(['CURRENT_REVISION'])
2456 current_rev = data['current_revision']
2457 url = data['revisions'][current_rev]['fetch']['http']['url']
2458 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002459
dsansomee2d6fd92016-09-08 00:10:47 -07002460 def UpdateDescriptionRemote(self, description, force=False):
2461 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2462 if not force:
2463 ask_for_data(
2464 'The description cannot be modified while the issue has a pending '
2465 'unpublished edit. Either publish the edit in the Gerrit web UI '
2466 'or delete it.\n\n'
2467 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2468
2469 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2470 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002471 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002472 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002473
2474 def CloseIssue(self):
2475 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2476
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002477 def GetApprovingReviewers(self):
2478 """Returns a list of reviewers approving the change.
2479
2480 Note: not necessarily committers.
2481 """
2482 raise NotImplementedError()
2483
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002484 def SubmitIssue(self, wait_for_merge=True):
2485 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2486 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002487
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002488 def _GetChangeDetail(self, options=None, issue=None):
2489 options = options or []
2490 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002491 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002492 try:
2493 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2494 options, ignore_404=False)
2495 except gerrit_util.GerritError as e:
2496 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002497 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002498 raise
tandriic2405f52016-10-10 08:13:15 -07002499 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002500
agable32978d92016-11-01 12:55:02 -07002501 def _GetChangeCommit(self, issue=None):
2502 issue = issue or self.GetIssue()
2503 assert issue, 'issue is required to query Gerrit'
2504 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2505 if not data:
Aaron Gablea45ee112016-11-22 15:14:38 -08002506 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
agable32978d92016-11-01 12:55:02 -07002507 return data
2508
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002509 def CMDLand(self, force, bypass_hooks, verbose):
2510 if git_common.is_dirty_git_tree('land'):
2511 return 1
tandriid60367b2016-06-22 05:25:12 -07002512 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2513 if u'Commit-Queue' in detail.get('labels', {}):
2514 if not force:
2515 ask_for_data('\nIt seems this repository has a Commit Queue, '
2516 'which can test and land changes for you. '
2517 'Are you sure you wish to bypass it?\n'
2518 'Press Enter to continue, Ctrl+C to abort.')
2519
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002520 differs = True
tandriic4344b52016-08-29 06:04:54 -07002521 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002522 # Note: git diff outputs nothing if there is no diff.
2523 if not last_upload or RunGit(['diff', last_upload]).strip():
2524 print('WARNING: some changes from local branch haven\'t been uploaded')
2525 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002526 if detail['current_revision'] == last_upload:
2527 differs = False
2528 else:
2529 print('WARNING: local branch contents differ from latest uploaded '
2530 'patchset')
2531 if differs:
2532 if not force:
2533 ask_for_data(
Andrii Shyshkalov3aac7752016-11-16 15:23:13 +01002534 'Do you want to submit latest Gerrit patchset and bypass hooks?\n'
2535 'Press Enter to continue, Ctrl+C to abort.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002536 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2537 elif not bypass_hooks:
2538 hook_results = self.RunHook(
2539 committing=True,
2540 may_prompt=not force,
2541 verbose=verbose,
2542 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2543 if not hook_results.should_continue():
2544 return 1
2545
2546 self.SubmitIssue(wait_for_merge=True)
2547 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002548 links = self._GetChangeCommit().get('web_links', [])
2549 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002550 if link.get('name') == 'gitiles' and link.get('url'):
agable32978d92016-11-01 12:55:02 -07002551 print('Landed as %s' % link.get('url'))
2552 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002553 return 0
2554
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002555 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2556 directory):
2557 assert not reject
2558 assert not nocommit
2559 assert not directory
2560 assert parsed_issue_arg.valid
2561
2562 self._changelist.issue = parsed_issue_arg.issue
2563
2564 if parsed_issue_arg.hostname:
2565 self._gerrit_host = parsed_issue_arg.hostname
2566 self._gerrit_server = 'https://%s' % self._gerrit_host
2567
tandriic2405f52016-10-10 08:13:15 -07002568 try:
2569 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002570 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002571 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002572
2573 if not parsed_issue_arg.patchset:
2574 # Use current revision by default.
2575 revision_info = detail['revisions'][detail['current_revision']]
2576 patchset = int(revision_info['_number'])
2577 else:
2578 patchset = parsed_issue_arg.patchset
2579 for revision_info in detail['revisions'].itervalues():
2580 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2581 break
2582 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002583 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002584 (parsed_issue_arg.patchset, self.GetIssue()))
2585
2586 fetch_info = revision_info['fetch']['http']
2587 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2588 RunGit(['cherry-pick', 'FETCH_HEAD'])
2589 self.SetIssue(self.GetIssue())
2590 self.SetPatchset(patchset)
Aaron Gablea45ee112016-11-22 15:14:38 -08002591 print('Committed patch for change %i patchset %i locally' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002592 (self.GetIssue(), self.GetPatchset()))
2593 return 0
2594
2595 @staticmethod
2596 def ParseIssueURL(parsed_url):
2597 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2598 return None
2599 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2600 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2601 # Short urls like https://domain/<issue_number> can be used, but don't allow
2602 # specifying the patchset (you'd 404), but we allow that here.
2603 if parsed_url.path == '/':
2604 part = parsed_url.fragment
2605 else:
2606 part = parsed_url.path
2607 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2608 if match:
2609 return _ParsedIssueNumberArgument(
2610 issue=int(match.group(2)),
2611 patchset=int(match.group(4)) if match.group(4) else None,
2612 hostname=parsed_url.netloc)
2613 return None
2614
tandrii16e0b4e2016-06-07 10:34:28 -07002615 def _GerritCommitMsgHookCheck(self, offer_removal):
2616 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2617 if not os.path.exists(hook):
2618 return
2619 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2620 # custom developer made one.
2621 data = gclient_utils.FileRead(hook)
2622 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2623 return
2624 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002625 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002626 'and may interfere with it in subtle ways.\n'
2627 'We recommend you remove the commit-msg hook.')
2628 if offer_removal:
2629 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2630 if reply.lower().startswith('y'):
2631 gclient_utils.rm_file_or_tree(hook)
2632 print('Gerrit commit-msg hook removed.')
2633 else:
2634 print('OK, will keep Gerrit commit-msg hook in place.')
2635
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002636 def CMDUploadChange(self, options, args, change):
2637 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002638 if options.squash and options.no_squash:
2639 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002640
2641 if not options.squash and not options.no_squash:
2642 # Load default for user, repo, squash=true, in this order.
2643 options.squash = settings.GetSquashGerritUploads()
2644 elif options.no_squash:
2645 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002646
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002647 # We assume the remote called "origin" is the one we want.
2648 # It is probably not worthwhile to support different workflows.
2649 gerrit_remote = 'origin'
2650
2651 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01002652 # Gerrit will not support pending prefix at all.
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002653 branch = GetTargetRef(remote, remote_branch, options.target_branch,
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01002654 pending_prefix_check=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002655
Aaron Gableb56ad332017-01-06 15:24:31 -08002656 # This may be None; default fallback value is determined in logic below.
2657 title = options.title
2658
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002659 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002660 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002661 if self.GetIssue():
2662 # Try to get the message from a previous upload.
2663 message = self.GetDescription()
2664 if not message:
2665 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002666 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002667 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002668 if not title:
2669 default_title = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
2670 title = ask_for_data(
2671 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002672 change_id = self._GetChangeDetail()['change_id']
2673 while True:
2674 footer_change_ids = git_footers.get_footer_change_id(message)
2675 if footer_change_ids == [change_id]:
2676 break
2677 if not footer_change_ids:
2678 message = git_footers.add_footer_change_id(message, change_id)
Aaron Gablea45ee112016-11-22 15:14:38 -08002679 print('WARNING: appended missing Change-Id to change description')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002680 continue
2681 # There is already a valid footer but with different or several ids.
2682 # Doing this automatically is non-trivial as we don't want to lose
2683 # existing other footers, yet we want to append just 1 desired
2684 # Change-Id. Thus, just create a new footer, but let user verify the
2685 # new description.
2686 message = '%s\n\nChange-Id: %s' % (message, change_id)
2687 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002688 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002689 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002690 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002691 'Please, check the proposed correction to the description, '
2692 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2693 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2694 change_id))
2695 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2696 if not options.force:
2697 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002698 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002699 message = change_desc.description
2700 if not message:
2701 DieWithError("Description is empty. Aborting...")
2702 # Continue the while loop.
2703 # Sanity check of this code - we should end up with proper message
2704 # footer.
2705 assert [change_id] == git_footers.get_footer_change_id(message)
2706 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002707 else: # if not self.GetIssue()
2708 if options.message:
2709 message = options.message
2710 else:
2711 message = CreateDescriptionFromLog(args)
2712 if options.title:
2713 message = options.title + '\n\n' + message
2714 change_desc = ChangeDescription(message)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002715 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002716 change_desc.prompt(bug=options.bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002717 # On first upload, patchset title is always this string, while
2718 # --title flag gets converted to first line of message.
2719 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002720 if not change_desc.description:
2721 DieWithError("Description is empty. Aborting...")
2722 message = change_desc.description
2723 change_ids = git_footers.get_footer_change_id(message)
2724 if len(change_ids) > 1:
2725 DieWithError('too many Change-Id footers, at most 1 allowed.')
2726 if not change_ids:
2727 # Generate the Change-Id automatically.
2728 message = git_footers.add_footer_change_id(
2729 message, GenerateGerritChangeId(message))
2730 change_desc.set_description(message)
2731 change_ids = git_footers.get_footer_change_id(message)
2732 assert len(change_ids) == 1
2733 change_id = change_ids[0]
2734
2735 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2736 if remote is '.':
2737 # If our upstream branch is local, we base our squashed commit on its
2738 # squashed version.
2739 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2740 # Check the squashed hash of the parent.
2741 parent = RunGit(['config',
2742 'branch.%s.gerritsquashhash' % upstream_branch_name],
2743 error_ok=True).strip()
2744 # Verify that the upstream branch has been uploaded too, otherwise
2745 # Gerrit will create additional CLs when uploading.
2746 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2747 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002748 DieWithError(
Aaron Gable48746652017-01-06 11:49:21 -08002749 '\nUpload upstream branch %s first.\n'
2750 'It is likely that this branch has been rebased since its last '
2751 'upload, so you just need to upload it again.\n'
2752 '(If you uploaded it with --no-squash, then branch dependencies '
2753 'are not supported, and you should reupload with --squash.)'
2754 % upstream_branch_name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002755 else:
2756 parent = self.GetCommonAncestorWithUpstream()
2757
2758 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2759 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2760 '-m', message]).strip()
2761 else:
2762 change_desc = ChangeDescription(
2763 options.message or CreateDescriptionFromLog(args))
2764 if not change_desc.description:
2765 DieWithError("Description is empty. Aborting...")
2766
2767 if not git_footers.get_footer_change_id(change_desc.description):
2768 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002769 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2770 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002771 ref_to_push = 'HEAD'
2772 parent = '%s/%s' % (gerrit_remote, branch)
2773 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2774
2775 assert change_desc
2776 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2777 ref_to_push)]).splitlines()
2778 if len(commits) > 1:
2779 print('WARNING: This will upload %d commits. Run the following command '
2780 'to see which commits will be uploaded: ' % len(commits))
2781 print('git log %s..%s' % (parent, ref_to_push))
2782 print('You can also use `git squash-branch` to squash these into a '
2783 'single commit.')
2784 ask_for_data('About to upload; enter to confirm.')
2785
2786 if options.reviewers or options.tbr_owners:
2787 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2788 change)
2789
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002790 # Extra options that can be specified at push time. Doc:
2791 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2792 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002793 if change_desc.get_reviewers(tbr_only=True):
2794 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2795 refspec_opts.append('l=Code-Review+1')
2796
Aaron Gable9b713dd2016-12-14 16:04:21 -08002797 if title:
2798 if not re.match(r'^[\w ]+$', title):
2799 title = re.sub(r'[^\w ]', '', title)
tandriieefe8322016-08-17 10:12:24 -07002800 print('WARNING: Patchset title may only contain alphanumeric chars '
Aaron Gable9b713dd2016-12-14 16:04:21 -08002801 'and spaces. Cleaned up title:\n%s' % title)
tandriieefe8322016-08-17 10:12:24 -07002802 if not options.force:
2803 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002804 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2805 # reverse on its side.
Aaron Gable9b713dd2016-12-14 16:04:21 -08002806 refspec_opts.append('m=' + title.replace(' ', '_'))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002807
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002808 if options.send_mail:
2809 if not change_desc.get_reviewers():
2810 DieWithError('Must specify reviewers to send email.')
2811 refspec_opts.append('notify=ALL')
2812 else:
2813 refspec_opts.append('notify=NONE')
2814
tandrii99a72f22016-08-17 14:33:24 -07002815 reviewers = change_desc.get_reviewers()
2816 if reviewers:
2817 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002818
agablec6787972016-09-09 16:13:34 -07002819 if options.private:
2820 refspec_opts.append('draft')
2821
rmistry9eadede2016-09-19 11:22:43 -07002822 if options.topic:
2823 # Documentation on Gerrit topics is here:
2824 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2825 refspec_opts.append('topic=%s' % options.topic)
2826
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002827 refspec_suffix = ''
2828 if refspec_opts:
2829 refspec_suffix = '%' + ','.join(refspec_opts)
2830 assert ' ' not in refspec_suffix, (
2831 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002832 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002833
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002834 try:
2835 push_stdout = gclient_utils.CheckCallAndFilter(
2836 ['git', 'push', gerrit_remote, refspec],
2837 print_stdout=True,
2838 # Flush after every line: useful for seeing progress when running as
2839 # recipe.
2840 filter_fn=lambda _: sys.stdout.flush())
2841 except subprocess2.CalledProcessError:
2842 DieWithError('Failed to create a change. Please examine output above '
2843 'for the reason of the failure. ')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002844
2845 if options.squash:
2846 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2847 change_numbers = [m.group(1)
2848 for m in map(regex.match, push_stdout.splitlines())
2849 if m]
2850 if len(change_numbers) != 1:
2851 DieWithError(
2852 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2853 'Change-Id: %s') % (len(change_numbers), change_id))
2854 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002855 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002856
2857 # Add cc's from the CC_LIST and --cc flag (if any).
2858 cc = self.GetCCList().split(',')
2859 if options.cc:
2860 cc.extend(options.cc)
2861 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002862 if change_desc.get_cced():
2863 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002864 if cc:
Aaron Gable5db82092016-11-17 10:52:08 -08002865 gerrit_util.AddReviewers(
Aaron Gable59f48512017-01-12 10:54:46 -08002866 self._GetGerritHost(), self.GetIssue(), cc,
2867 is_reviewer=False, notify=bool(options.send_mail))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002868 return 0
2869
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002870 def _AddChangeIdToCommitMessage(self, options, args):
2871 """Re-commits using the current message, assumes the commit hook is in
2872 place.
2873 """
2874 log_desc = options.message or CreateDescriptionFromLog(args)
2875 git_command = ['commit', '--amend', '-m', log_desc]
2876 RunGit(git_command)
2877 new_log_desc = CreateDescriptionFromLog(args)
2878 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002879 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002880 return new_log_desc
2881 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002882 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002883
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002884 def SetCQState(self, new_state):
2885 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002886 vote_map = {
2887 _CQState.NONE: 0,
2888 _CQState.DRY_RUN: 1,
2889 _CQState.COMMIT : 2,
2890 }
Andrii Shyshkalov828701b2016-12-09 10:46:47 +01002891 kwargs = {'labels': {'Commit-Queue': vote_map[new_state]}}
2892 if new_state == _CQState.DRY_RUN:
2893 # Don't spam everybody reviewer/owner.
2894 kwargs['notify'] = 'NONE'
2895 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(), **kwargs)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002896
tandriie113dfd2016-10-11 10:20:12 -07002897 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002898 try:
2899 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002900 except GerritChangeNotExists:
2901 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002902
2903 if data['status'] in ('ABANDONED', 'MERGED'):
2904 return 'CL %s is closed' % self.GetIssue()
2905
2906 def GetTryjobProperties(self, patchset=None):
2907 """Returns dictionary of properties to launch tryjob."""
2908 data = self._GetChangeDetail(['ALL_REVISIONS'])
2909 patchset = int(patchset or self.GetPatchset())
2910 assert patchset
2911 revision_data = None # Pylint wants it to be defined.
2912 for revision_data in data['revisions'].itervalues():
2913 if int(revision_data['_number']) == patchset:
2914 break
2915 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002916 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002917 (patchset, self.GetIssue()))
2918 return {
2919 'patch_issue': self.GetIssue(),
2920 'patch_set': patchset or self.GetPatchset(),
2921 'patch_project': data['project'],
2922 'patch_storage': 'gerrit',
2923 'patch_ref': revision_data['fetch']['http']['ref'],
2924 'patch_repository_url': revision_data['fetch']['http']['url'],
2925 'patch_gerrit_url': self.GetCodereviewServer(),
2926 }
tandriie113dfd2016-10-11 10:20:12 -07002927
tandriide281ae2016-10-12 06:02:30 -07002928 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002929 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002930
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002931
2932_CODEREVIEW_IMPLEMENTATIONS = {
2933 'rietveld': _RietveldChangelistImpl,
2934 'gerrit': _GerritChangelistImpl,
2935}
2936
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002937
iannuccie53c9352016-08-17 14:40:40 -07002938def _add_codereview_issue_select_options(parser, extra=""):
2939 _add_codereview_select_options(parser)
2940
2941 text = ('Operate on this issue number instead of the current branch\'s '
2942 'implicit issue.')
2943 if extra:
2944 text += ' '+extra
2945 parser.add_option('-i', '--issue', type=int, help=text)
2946
2947
2948def _process_codereview_issue_select_options(parser, options):
2949 _process_codereview_select_options(parser, options)
2950 if options.issue is not None and not options.forced_codereview:
2951 parser.error('--issue must be specified with either --rietveld or --gerrit')
2952
2953
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002954def _add_codereview_select_options(parser):
2955 """Appends --gerrit and --rietveld options to force specific codereview."""
2956 parser.codereview_group = optparse.OptionGroup(
2957 parser, 'EXPERIMENTAL! Codereview override options')
2958 parser.add_option_group(parser.codereview_group)
2959 parser.codereview_group.add_option(
2960 '--gerrit', action='store_true',
2961 help='Force the use of Gerrit for codereview')
2962 parser.codereview_group.add_option(
2963 '--rietveld', action='store_true',
2964 help='Force the use of Rietveld for codereview')
2965
2966
2967def _process_codereview_select_options(parser, options):
2968 if options.gerrit and options.rietveld:
2969 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2970 options.forced_codereview = None
2971 if options.gerrit:
2972 options.forced_codereview = 'gerrit'
2973 elif options.rietveld:
2974 options.forced_codereview = 'rietveld'
2975
2976
tandriif9aefb72016-07-01 09:06:51 -07002977def _get_bug_line_values(default_project, bugs):
2978 """Given default_project and comma separated list of bugs, yields bug line
2979 values.
2980
2981 Each bug can be either:
2982 * a number, which is combined with default_project
2983 * string, which is left as is.
2984
2985 This function may produce more than one line, because bugdroid expects one
2986 project per line.
2987
2988 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2989 ['v8:123', 'chromium:789']
2990 """
2991 default_bugs = []
2992 others = []
2993 for bug in bugs.split(','):
2994 bug = bug.strip()
2995 if bug:
2996 try:
2997 default_bugs.append(int(bug))
2998 except ValueError:
2999 others.append(bug)
3000
3001 if default_bugs:
3002 default_bugs = ','.join(map(str, default_bugs))
3003 if default_project:
3004 yield '%s:%s' % (default_project, default_bugs)
3005 else:
3006 yield default_bugs
3007 for other in sorted(others):
3008 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3009 yield other
3010
3011
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003012class ChangeDescription(object):
3013 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003014 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003015 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00003016 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003017 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003018
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003019 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003020 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003021
agable@chromium.org42c20792013-09-12 17:34:49 +00003022 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003023 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003024 return '\n'.join(self._description_lines)
3025
3026 def set_description(self, desc):
3027 if isinstance(desc, basestring):
3028 lines = desc.splitlines()
3029 else:
3030 lines = [line.rstrip() for line in desc]
3031 while lines and not lines[0]:
3032 lines.pop(0)
3033 while lines and not lines[-1]:
3034 lines.pop(-1)
3035 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003036
piman@chromium.org336f9122014-09-04 02:16:55 +00003037 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003038 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003039 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00003040 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003041 return
agable@chromium.org42c20792013-09-12 17:34:49 +00003042 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003043
agable@chromium.org42c20792013-09-12 17:34:49 +00003044 # Get the set of R= and TBR= lines and remove them from the desciption.
3045 regexp = re.compile(self.R_LINE)
3046 matches = [regexp.match(line) for line in self._description_lines]
3047 new_desc = [l for i, l in enumerate(self._description_lines)
3048 if not matches[i]]
3049 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003050
agable@chromium.org42c20792013-09-12 17:34:49 +00003051 # Construct new unified R= and TBR= lines.
3052 r_names = []
3053 tbr_names = []
3054 for match in matches:
3055 if not match:
3056 continue
3057 people = cleanup_list([match.group(2).strip()])
3058 if match.group(1) == 'TBR':
3059 tbr_names.extend(people)
3060 else:
3061 r_names.extend(people)
3062 for name in r_names:
3063 if name not in reviewers:
3064 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003065 if add_owners_tbr:
3066 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003067 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003068 all_reviewers = set(tbr_names + reviewers)
3069 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3070 all_reviewers)
3071 tbr_names.extend(owners_db.reviewers_for(missing_files,
3072 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003073 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3074 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3075
3076 # Put the new lines in the description where the old first R= line was.
3077 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3078 if 0 <= line_loc < len(self._description_lines):
3079 if new_tbr_line:
3080 self._description_lines.insert(line_loc, new_tbr_line)
3081 if new_r_line:
3082 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003083 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003084 if new_r_line:
3085 self.append_footer(new_r_line)
3086 if new_tbr_line:
3087 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003088
tandriif9aefb72016-07-01 09:06:51 -07003089 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003090 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003091 self.set_description([
3092 '# Enter a description of the change.',
3093 '# This will be displayed on the codereview site.',
3094 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003095 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003096 '--------------------',
3097 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003098
agable@chromium.org42c20792013-09-12 17:34:49 +00003099 regexp = re.compile(self.BUG_LINE)
3100 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003101 prefix = settings.GetBugPrefix()
3102 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
3103 for value in values:
3104 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
3105 self.append_footer('BUG=%s' % value)
3106
agable@chromium.org42c20792013-09-12 17:34:49 +00003107 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003108 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003109 if not content:
3110 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003111 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003112
3113 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003114 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3115 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003116 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003117 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003118
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003119 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003120 """Adds a footer line to the description.
3121
3122 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3123 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3124 that Gerrit footers are always at the end.
3125 """
3126 parsed_footer_line = git_footers.parse_footer(line)
3127 if parsed_footer_line:
3128 # Line is a gerrit footer in the form: Footer-Key: any value.
3129 # Thus, must be appended observing Gerrit footer rules.
3130 self.set_description(
3131 git_footers.add_footer(self.description,
3132 key=parsed_footer_line[0],
3133 value=parsed_footer_line[1]))
3134 return
3135
3136 if not self._description_lines:
3137 self._description_lines.append(line)
3138 return
3139
3140 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3141 if gerrit_footers:
3142 # git_footers.split_footers ensures that there is an empty line before
3143 # actual (gerrit) footers, if any. We have to keep it that way.
3144 assert top_lines and top_lines[-1] == ''
3145 top_lines, separator = top_lines[:-1], top_lines[-1:]
3146 else:
3147 separator = [] # No need for separator if there are no gerrit_footers.
3148
3149 prev_line = top_lines[-1] if top_lines else ''
3150 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3151 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3152 top_lines.append('')
3153 top_lines.append(line)
3154 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003155
tandrii99a72f22016-08-17 14:33:24 -07003156 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003157 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003158 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003159 reviewers = [match.group(2).strip()
3160 for match in matches
3161 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003162 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003163
bradnelsond975b302016-10-23 12:20:23 -07003164 def get_cced(self):
3165 """Retrieves the list of reviewers."""
3166 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3167 cced = [match.group(2).strip() for match in matches if match]
3168 return cleanup_list(cced)
3169
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003170 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3171 """Updates this commit description given the parent.
3172
3173 This is essentially what Gnumbd used to do.
3174 Consult https://goo.gl/WMmpDe for more details.
3175 """
3176 assert parent_msg # No, orphan branch creation isn't supported.
3177 assert parent_hash
3178 assert dest_ref
3179 parent_footer_map = git_footers.parse_footers(parent_msg)
3180 # This will also happily parse svn-position, which GnumbD is no longer
3181 # supporting. While we'd generate correct footers, the verifier plugin
3182 # installed in Gerrit will block such commit (ie git push below will fail).
3183 parent_position = git_footers.get_position(parent_footer_map)
3184
3185 # Cherry-picks may have last line obscuring their prior footers,
3186 # from git_footers perspective. This is also what Gnumbd did.
3187 cp_line = None
3188 if (self._description_lines and
3189 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3190 cp_line = self._description_lines.pop()
3191
3192 top_lines, _, parsed_footers = git_footers.split_footers(self.description)
3193
3194 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3195 # user interference with actual footers we'd insert below.
3196 for i, (k, v) in enumerate(parsed_footers):
3197 if k.startswith('Cr-'):
3198 parsed_footers[i] = (k.replace('Cr-', 'Cr-Original-'), v)
3199
3200 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003201 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003202 if parent_position[0] == dest_ref:
3203 # Same branch as parent.
3204 number = int(parent_position[1]) + 1
3205 else:
3206 number = 1 # New branch, and extra lineage.
3207 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3208 int(parent_position[1])))
3209
3210 parsed_footers.append(('Cr-Commit-Position',
3211 '%s@{#%d}' % (dest_ref, number)))
3212 parsed_footers.extend(('Cr-Branched-From', v) for v in lineage)
3213
3214 self._description_lines = top_lines
3215 if cp_line:
3216 self._description_lines.append(cp_line)
3217 if self._description_lines[-1] != '':
3218 self._description_lines.append('') # Ensure footer separator.
3219 self._description_lines.extend('%s: %s' % kv for kv in parsed_footers)
3220
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003221
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003222def get_approving_reviewers(props):
3223 """Retrieves the reviewers that approved a CL from the issue properties with
3224 messages.
3225
3226 Note that the list may contain reviewers that are not committer, thus are not
3227 considered by the CQ.
3228 """
3229 return sorted(
3230 set(
3231 message['sender']
3232 for message in props['messages']
3233 if message['approval'] and message['sender'] in props['reviewers']
3234 )
3235 )
3236
3237
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003238def FindCodereviewSettingsFile(filename='codereview.settings'):
3239 """Finds the given file starting in the cwd and going up.
3240
3241 Only looks up to the top of the repository unless an
3242 'inherit-review-settings-ok' file exists in the root of the repository.
3243 """
3244 inherit_ok_file = 'inherit-review-settings-ok'
3245 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003246 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003247 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3248 root = '/'
3249 while True:
3250 if filename in os.listdir(cwd):
3251 if os.path.isfile(os.path.join(cwd, filename)):
3252 return open(os.path.join(cwd, filename))
3253 if cwd == root:
3254 break
3255 cwd = os.path.dirname(cwd)
3256
3257
3258def LoadCodereviewSettingsFromFile(fileobj):
3259 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003260 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003261
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003262 def SetProperty(name, setting, unset_error_ok=False):
3263 fullname = 'rietveld.' + name
3264 if setting in keyvals:
3265 RunGit(['config', fullname, keyvals[setting]])
3266 else:
3267 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3268
tandrii48df5812016-10-17 03:55:37 -07003269 if not keyvals.get('GERRIT_HOST', False):
3270 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003271 # Only server setting is required. Other settings can be absent.
3272 # In that case, we ignore errors raised during option deletion attempt.
3273 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003274 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003275 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3276 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003277 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003278 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3279 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003280 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003281 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003282 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3283 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003284
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003285 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003286 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003287
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003288 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003289 RunGit(['config', 'gerrit.squash-uploads',
3290 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003291
tandrii@chromium.org28253532016-04-14 13:46:56 +00003292 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003293 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003294 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3295
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003296 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3297 #should be of the form
3298 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3299 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
3300 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3301 keyvals['ORIGIN_URL_CONFIG']])
3302
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003303
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003304def urlretrieve(source, destination):
3305 """urllib is broken for SSL connections via a proxy therefore we
3306 can't use urllib.urlretrieve()."""
3307 with open(destination, 'w') as f:
3308 f.write(urllib2.urlopen(source).read())
3309
3310
ukai@chromium.org712d6102013-11-27 00:52:58 +00003311def hasSheBang(fname):
3312 """Checks fname is a #! script."""
3313 with open(fname) as f:
3314 return f.read(2).startswith('#!')
3315
3316
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003317# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3318def DownloadHooks(*args, **kwargs):
3319 pass
3320
3321
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003322def DownloadGerritHook(force):
3323 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003324
3325 Args:
3326 force: True to update hooks. False to install hooks if not present.
3327 """
3328 if not settings.GetIsGerrit():
3329 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003330 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003331 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3332 if not os.access(dst, os.X_OK):
3333 if os.path.exists(dst):
3334 if not force:
3335 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003336 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003337 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003338 if not hasSheBang(dst):
3339 DieWithError('Not a script: %s\n'
3340 'You need to download from\n%s\n'
3341 'into .git/hooks/commit-msg and '
3342 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003343 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3344 except Exception:
3345 if os.path.exists(dst):
3346 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003347 DieWithError('\nFailed to download hooks.\n'
3348 'You need to download from\n%s\n'
3349 'into .git/hooks/commit-msg and '
3350 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003351
3352
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003353
3354def GetRietveldCodereviewSettingsInteractively():
3355 """Prompt the user for settings."""
3356 server = settings.GetDefaultServerUrl(error_ok=True)
3357 prompt = 'Rietveld server (host[:port])'
3358 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3359 newserver = ask_for_data(prompt + ':')
3360 if not server and not newserver:
3361 newserver = DEFAULT_SERVER
3362 if newserver:
3363 newserver = gclient_utils.UpgradeToHttps(newserver)
3364 if newserver != server:
3365 RunGit(['config', 'rietveld.server', newserver])
3366
3367 def SetProperty(initial, caption, name, is_url):
3368 prompt = caption
3369 if initial:
3370 prompt += ' ("x" to clear) [%s]' % initial
3371 new_val = ask_for_data(prompt + ':')
3372 if new_val == 'x':
3373 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3374 elif new_val:
3375 if is_url:
3376 new_val = gclient_utils.UpgradeToHttps(new_val)
3377 if new_val != initial:
3378 RunGit(['config', 'rietveld.' + name, new_val])
3379
3380 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3381 SetProperty(settings.GetDefaultPrivateFlag(),
3382 'Private flag (rietveld only)', 'private', False)
3383 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3384 'tree-status-url', False)
3385 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3386 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3387 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3388 'run-post-upload-hook', False)
3389
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003390@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003391def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003392 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003393
tandrii5d0a0422016-09-14 06:24:35 -07003394 print('WARNING: git cl config works for Rietveld only')
3395 # TODO(tandrii): remove this once we switch to Gerrit.
3396 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003397 parser.add_option('--activate-update', action='store_true',
3398 help='activate auto-updating [rietveld] section in '
3399 '.git/config')
3400 parser.add_option('--deactivate-update', action='store_true',
3401 help='deactivate auto-updating [rietveld] section in '
3402 '.git/config')
3403 options, args = parser.parse_args(args)
3404
3405 if options.deactivate_update:
3406 RunGit(['config', 'rietveld.autoupdate', 'false'])
3407 return
3408
3409 if options.activate_update:
3410 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3411 return
3412
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003413 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003414 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003415 return 0
3416
3417 url = args[0]
3418 if not url.endswith('codereview.settings'):
3419 url = os.path.join(url, 'codereview.settings')
3420
3421 # Load code review settings and download hooks (if available).
3422 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3423 return 0
3424
3425
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003426def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003427 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003428 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3429 branch = ShortBranchName(branchref)
3430 _, args = parser.parse_args(args)
3431 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003432 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003433 return RunGit(['config', 'branch.%s.base-url' % branch],
3434 error_ok=False).strip()
3435 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003436 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003437 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3438 error_ok=False).strip()
3439
3440
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003441def color_for_status(status):
3442 """Maps a Changelist status to color, for CMDstatus and other tools."""
3443 return {
3444 'unsent': Fore.RED,
3445 'waiting': Fore.BLUE,
3446 'reply': Fore.YELLOW,
3447 'lgtm': Fore.GREEN,
3448 'commit': Fore.MAGENTA,
3449 'closed': Fore.CYAN,
3450 'error': Fore.WHITE,
3451 }.get(status, Fore.WHITE)
3452
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003453
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003454def get_cl_statuses(changes, fine_grained, max_processes=None):
3455 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003456
3457 If fine_grained is true, this will fetch CL statuses from the server.
3458 Otherwise, simply indicate if there's a matching url for the given branches.
3459
3460 If max_processes is specified, it is used as the maximum number of processes
3461 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3462 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003463
3464 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003465 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003466 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003467 upload.verbosity = 0
3468
3469 if fine_grained:
3470 # Process one branch synchronously to work through authentication, then
3471 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003472 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003473 def fetch(cl):
3474 try:
3475 return (cl, cl.GetStatus())
3476 except:
3477 # See http://crbug.com/629863.
3478 logging.exception('failed to fetch status for %s:', cl)
3479 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003480 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003481
tandriiea9514a2016-08-17 12:32:37 -07003482 changes_to_fetch = changes[1:]
3483 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003484 # Exit early if there was only one branch to fetch.
3485 return
3486
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003487 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003488 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003489 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003490 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003491
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003492 fetched_cls = set()
3493 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003494 while True:
3495 try:
3496 row = it.next(timeout=5)
3497 except multiprocessing.TimeoutError:
3498 break
3499
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003500 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003501 yield row
3502
3503 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003504 for cl in set(changes_to_fetch) - fetched_cls:
3505 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003506
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003507 else:
3508 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003509 for cl in changes:
3510 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003511
rmistry@google.com2dd99862015-06-22 12:22:18 +00003512
3513def upload_branch_deps(cl, args):
3514 """Uploads CLs of local branches that are dependents of the current branch.
3515
3516 If the local branch dependency tree looks like:
3517 test1 -> test2.1 -> test3.1
3518 -> test3.2
3519 -> test2.2 -> test3.3
3520
3521 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3522 run on the dependent branches in this order:
3523 test2.1, test3.1, test3.2, test2.2, test3.3
3524
3525 Note: This function does not rebase your local dependent branches. Use it when
3526 you make a change to the parent branch that will not conflict with its
3527 dependent branches, and you would like their dependencies updated in
3528 Rietveld.
3529 """
3530 if git_common.is_dirty_git_tree('upload-branch-deps'):
3531 return 1
3532
3533 root_branch = cl.GetBranch()
3534 if root_branch is None:
3535 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3536 'Get on a branch!')
3537 if not cl.GetIssue() or not cl.GetPatchset():
3538 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3539 'patchset dependencies without an uploaded CL.')
3540
3541 branches = RunGit(['for-each-ref',
3542 '--format=%(refname:short) %(upstream:short)',
3543 'refs/heads'])
3544 if not branches:
3545 print('No local branches found.')
3546 return 0
3547
3548 # Create a dictionary of all local branches to the branches that are dependent
3549 # on it.
3550 tracked_to_dependents = collections.defaultdict(list)
3551 for b in branches.splitlines():
3552 tokens = b.split()
3553 if len(tokens) == 2:
3554 branch_name, tracked = tokens
3555 tracked_to_dependents[tracked].append(branch_name)
3556
vapiera7fbd5a2016-06-16 09:17:49 -07003557 print()
3558 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003559 dependents = []
3560 def traverse_dependents_preorder(branch, padding=''):
3561 dependents_to_process = tracked_to_dependents.get(branch, [])
3562 padding += ' '
3563 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003564 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003565 dependents.append(dependent)
3566 traverse_dependents_preorder(dependent, padding)
3567 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003568 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003569
3570 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003571 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003572 return 0
3573
vapiera7fbd5a2016-06-16 09:17:49 -07003574 print('This command will checkout all dependent branches and run '
3575 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003576 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3577
andybons@chromium.org962f9462016-02-03 20:00:42 +00003578 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003579 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003580 args.extend(['-t', 'Updated patchset dependency'])
3581
rmistry@google.com2dd99862015-06-22 12:22:18 +00003582 # Record all dependents that failed to upload.
3583 failures = {}
3584 # Go through all dependents, checkout the branch and upload.
3585 try:
3586 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003587 print()
3588 print('--------------------------------------')
3589 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003590 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003591 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003592 try:
3593 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003594 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003595 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003596 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003597 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003598 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003599 finally:
3600 # Swap back to the original root branch.
3601 RunGit(['checkout', '-q', root_branch])
3602
vapiera7fbd5a2016-06-16 09:17:49 -07003603 print()
3604 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003605 for dependent_branch in dependents:
3606 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003607 print(' %s : %s' % (dependent_branch, upload_status))
3608 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003609
3610 return 0
3611
3612
kmarshall3bff56b2016-06-06 18:31:47 -07003613def CMDarchive(parser, args):
3614 """Archives and deletes branches associated with closed changelists."""
3615 parser.add_option(
3616 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003617 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003618 parser.add_option(
3619 '-f', '--force', action='store_true',
3620 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003621 parser.add_option(
3622 '-d', '--dry-run', action='store_true',
3623 help='Skip the branch tagging and removal steps.')
3624 parser.add_option(
3625 '-t', '--notags', action='store_true',
3626 help='Do not tag archived branches. '
3627 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003628
3629 auth.add_auth_options(parser)
3630 options, args = parser.parse_args(args)
3631 if args:
3632 parser.error('Unsupported args: %s' % ' '.join(args))
3633 auth_config = auth.extract_auth_config_from_options(options)
3634
3635 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3636 if not branches:
3637 return 0
3638
vapiera7fbd5a2016-06-16 09:17:49 -07003639 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003640 changes = [Changelist(branchref=b, auth_config=auth_config)
3641 for b in branches.splitlines()]
3642 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3643 statuses = get_cl_statuses(changes,
3644 fine_grained=True,
3645 max_processes=options.maxjobs)
3646 proposal = [(cl.GetBranch(),
3647 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3648 for cl, status in statuses
3649 if status == 'closed']
3650 proposal.sort()
3651
3652 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003653 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003654 return 0
3655
3656 current_branch = GetCurrentBranch()
3657
vapiera7fbd5a2016-06-16 09:17:49 -07003658 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003659 if options.notags:
3660 for next_item in proposal:
3661 print(' ' + next_item[0])
3662 else:
3663 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3664 for next_item in proposal:
3665 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003666
kmarshall9249e012016-08-23 12:02:16 -07003667 # Quit now on precondition failure or if instructed by the user, either
3668 # via an interactive prompt or by command line flags.
3669 if options.dry_run:
3670 print('\nNo changes were made (dry run).\n')
3671 return 0
3672 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003673 print('You are currently on a branch \'%s\' which is associated with a '
3674 'closed codereview issue, so archive cannot proceed. Please '
3675 'checkout another branch and run this command again.' %
3676 current_branch)
3677 return 1
kmarshall9249e012016-08-23 12:02:16 -07003678 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003679 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3680 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003681 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003682 return 1
3683
3684 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003685 if not options.notags:
3686 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003687 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003688
vapiera7fbd5a2016-06-16 09:17:49 -07003689 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003690
3691 return 0
3692
3693
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003694def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003695 """Show status of changelists.
3696
3697 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003698 - Red not sent for review or broken
3699 - Blue waiting for review
3700 - Yellow waiting for you to reply to review
3701 - Green LGTM'ed
3702 - Magenta in the commit queue
3703 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003704
3705 Also see 'git cl comments'.
3706 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003707 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003708 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003709 parser.add_option('-f', '--fast', action='store_true',
3710 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003711 parser.add_option(
3712 '-j', '--maxjobs', action='store', type=int,
3713 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003714
3715 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003716 _add_codereview_issue_select_options(
3717 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003718 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003719 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003720 if args:
3721 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003722 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003723
iannuccie53c9352016-08-17 14:40:40 -07003724 if options.issue is not None and not options.field:
3725 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003726
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003727 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003728 cl = Changelist(auth_config=auth_config, issue=options.issue,
3729 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003730 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003731 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003732 elif options.field == 'id':
3733 issueid = cl.GetIssue()
3734 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003735 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003736 elif options.field == 'patch':
3737 patchset = cl.GetPatchset()
3738 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003739 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003740 elif options.field == 'status':
3741 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003742 elif options.field == 'url':
3743 url = cl.GetIssueURL()
3744 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003745 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003746 return 0
3747
3748 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3749 if not branches:
3750 print('No local branch found.')
3751 return 0
3752
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003753 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003754 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003755 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003756 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003757 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003758 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003759 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003760
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003761 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003762 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3763 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3764 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003765 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003766 c, status = output.next()
3767 branch_statuses[c.GetBranch()] = status
3768 status = branch_statuses.pop(branch)
3769 url = cl.GetIssueURL()
3770 if url and (not status or status == 'error'):
3771 # The issue probably doesn't exist anymore.
3772 url += ' (broken)'
3773
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003774 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003775 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003776 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003777 color = ''
3778 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003779 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003780 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003781 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003782 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003783
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003784 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003785 print()
3786 print('Current branch:',)
3787 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003788 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003789 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003790 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003791 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003792 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003793 print('Issue description:')
3794 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003795 return 0
3796
3797
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003798def colorize_CMDstatus_doc():
3799 """To be called once in main() to add colors to git cl status help."""
3800 colors = [i for i in dir(Fore) if i[0].isupper()]
3801
3802 def colorize_line(line):
3803 for color in colors:
3804 if color in line.upper():
3805 # Extract whitespaces first and the leading '-'.
3806 indent = len(line) - len(line.lstrip(' ')) + 1
3807 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3808 return line
3809
3810 lines = CMDstatus.__doc__.splitlines()
3811 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3812
3813
phajdan.jre328cf92016-08-22 04:12:17 -07003814def write_json(path, contents):
3815 with open(path, 'w') as f:
3816 json.dump(contents, f)
3817
3818
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003819@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003820def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003821 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003822
3823 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003824 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003825 parser.add_option('-r', '--reverse', action='store_true',
3826 help='Lookup the branch(es) for the specified issues. If '
3827 'no issues are specified, all branches with mapped '
3828 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003829 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003830 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003831 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003832 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003833
dnj@chromium.org406c4402015-03-03 17:22:28 +00003834 if options.reverse:
3835 branches = RunGit(['for-each-ref', 'refs/heads',
3836 '--format=%(refname:short)']).splitlines()
3837
3838 # Reverse issue lookup.
3839 issue_branch_map = {}
3840 for branch in branches:
3841 cl = Changelist(branchref=branch)
3842 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3843 if not args:
3844 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003845 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003846 for issue in args:
3847 if not issue:
3848 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003849 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003850 print('Branch for issue number %s: %s' % (
3851 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003852 if options.json:
3853 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003854 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003855 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003856 if len(args) > 0:
3857 try:
3858 issue = int(args[0])
3859 except ValueError:
3860 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003861 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003862 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003863 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003864 if options.json:
3865 write_json(options.json, {
3866 'issue': cl.GetIssue(),
3867 'issue_url': cl.GetIssueURL(),
3868 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003869 return 0
3870
3871
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003872def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003873 """Shows or posts review comments for any changelist."""
3874 parser.add_option('-a', '--add-comment', dest='comment',
3875 help='comment to add to an issue')
3876 parser.add_option('-i', dest='issue',
3877 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003878 parser.add_option('-j', '--json-file',
3879 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003880 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003881 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003882 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003883
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003884 issue = None
3885 if options.issue:
3886 try:
3887 issue = int(options.issue)
3888 except ValueError:
3889 DieWithError('A review issue id is expected to be a number')
3890
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003891 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003892
3893 if options.comment:
3894 cl.AddComment(options.comment)
3895 return 0
3896
3897 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003898 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003899 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003900 summary.append({
3901 'date': message['date'],
3902 'lgtm': False,
3903 'message': message['text'],
3904 'not_lgtm': False,
3905 'sender': message['sender'],
3906 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003907 if message['disapproval']:
3908 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003909 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003910 elif message['approval']:
3911 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003912 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003913 elif message['sender'] == data['owner_email']:
3914 color = Fore.MAGENTA
3915 else:
3916 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003917 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003918 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003919 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003920 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003921 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003922 if options.json_file:
3923 with open(options.json_file, 'wb') as f:
3924 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003925 return 0
3926
3927
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003928@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003929def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003930 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003931 parser.add_option('-d', '--display', action='store_true',
3932 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003933 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003934 help='New description to set for this issue (- for stdin, '
3935 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003936 parser.add_option('-f', '--force', action='store_true',
3937 help='Delete any unpublished Gerrit edits for this issue '
3938 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003939
3940 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003941 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003942 options, args = parser.parse_args(args)
3943 _process_codereview_select_options(parser, options)
3944
3945 target_issue = None
3946 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003947 target_issue = ParseIssueNumberArgument(args[0])
3948 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003949 parser.print_help()
3950 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003951
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003952 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003953
martiniss6eda05f2016-06-30 10:18:35 -07003954 kwargs = {
3955 'auth_config': auth_config,
3956 'codereview': options.forced_codereview,
3957 }
3958 if target_issue:
3959 kwargs['issue'] = target_issue.issue
3960 if options.forced_codereview == 'rietveld':
3961 kwargs['rietveld_server'] = target_issue.hostname
3962
3963 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003964
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003965 if not cl.GetIssue():
3966 DieWithError('This branch has no associated changelist.')
3967 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003968
smut@google.com34fb6b12015-07-13 20:03:26 +00003969 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003970 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003971 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003972
3973 if options.new_description:
3974 text = options.new_description
3975 if text == '-':
3976 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003977 elif text == '+':
3978 base_branch = cl.GetCommonAncestorWithUpstream()
3979 change = cl.GetChange(base_branch, None, local_description=True)
3980 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003981
3982 description.set_description(text)
3983 else:
3984 description.prompt()
3985
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003986 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003987 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003988 return 0
3989
3990
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003991def CreateDescriptionFromLog(args):
3992 """Pulls out the commit log to use as a base for the CL description."""
3993 log_args = []
3994 if len(args) == 1 and not args[0].endswith('.'):
3995 log_args = [args[0] + '..']
3996 elif len(args) == 1 and args[0].endswith('...'):
3997 log_args = [args[0][:-1]]
3998 elif len(args) == 2:
3999 log_args = [args[0] + '..' + args[1]]
4000 else:
4001 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004002 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004003
4004
thestig@chromium.org44202a22014-03-11 19:22:18 +00004005def CMDlint(parser, args):
4006 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004007 parser.add_option('--filter', action='append', metavar='-x,+y',
4008 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004009 auth.add_auth_options(parser)
4010 options, args = parser.parse_args(args)
4011 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004012
4013 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004014 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004015 try:
4016 import cpplint
4017 import cpplint_chromium
4018 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004019 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004020 return 1
4021
4022 # Change the current working directory before calling lint so that it
4023 # shows the correct base.
4024 previous_cwd = os.getcwd()
4025 os.chdir(settings.GetRoot())
4026 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004027 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004028 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4029 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004030 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004031 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004032 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004033
4034 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004035 command = args + files
4036 if options.filter:
4037 command = ['--filter=' + ','.join(options.filter)] + command
4038 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004039
4040 white_regex = re.compile(settings.GetLintRegex())
4041 black_regex = re.compile(settings.GetLintIgnoreRegex())
4042 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4043 for filename in filenames:
4044 if white_regex.match(filename):
4045 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004046 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004047 else:
4048 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4049 extra_check_functions)
4050 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004051 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004052 finally:
4053 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004054 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004055 if cpplint._cpplint_state.error_count != 0:
4056 return 1
4057 return 0
4058
4059
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004060def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004061 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004062 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004063 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004064 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004065 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004066 auth.add_auth_options(parser)
4067 options, args = parser.parse_args(args)
4068 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004069
sbc@chromium.org71437c02015-04-09 19:29:40 +00004070 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004071 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004072 return 1
4073
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004074 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004075 if args:
4076 base_branch = args[0]
4077 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004078 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004079 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004080
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004081 cl.RunHook(
4082 committing=not options.upload,
4083 may_prompt=False,
4084 verbose=options.verbose,
4085 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004086 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004087
4088
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004089def GenerateGerritChangeId(message):
4090 """Returns Ixxxxxx...xxx change id.
4091
4092 Works the same way as
4093 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4094 but can be called on demand on all platforms.
4095
4096 The basic idea is to generate git hash of a state of the tree, original commit
4097 message, author/committer info and timestamps.
4098 """
4099 lines = []
4100 tree_hash = RunGitSilent(['write-tree'])
4101 lines.append('tree %s' % tree_hash.strip())
4102 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4103 if code == 0:
4104 lines.append('parent %s' % parent.strip())
4105 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4106 lines.append('author %s' % author.strip())
4107 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4108 lines.append('committer %s' % committer.strip())
4109 lines.append('')
4110 # Note: Gerrit's commit-hook actually cleans message of some lines and
4111 # whitespace. This code is not doing this, but it clearly won't decrease
4112 # entropy.
4113 lines.append(message)
4114 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4115 stdin='\n'.join(lines))
4116 return 'I%s' % change_hash.strip()
4117
4118
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004119def GetTargetRef(remote, remote_branch, target_branch, pending_prefix_check,
4120 remote_url=None):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004121 """Computes the remote branch ref to use for the CL.
4122
4123 Args:
4124 remote (str): The git remote for the CL.
4125 remote_branch (str): The git remote branch for the CL.
4126 target_branch (str): The target branch specified by the user.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004127 pending_prefix_check (bool): If true, determines if pending_prefix should be
4128 used.
4129 remote_url (str): Only used for checking if pending_prefix should be used.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004130 """
4131 if not (remote and remote_branch):
4132 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004133
wittman@chromium.org455dc922015-01-26 20:15:50 +00004134 if target_branch:
4135 # Cannonicalize branch references to the equivalent local full symbolic
4136 # refs, which are then translated into the remote full symbolic refs
4137 # below.
4138 if '/' not in target_branch:
4139 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4140 else:
4141 prefix_replacements = (
4142 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4143 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4144 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4145 )
4146 match = None
4147 for regex, replacement in prefix_replacements:
4148 match = re.search(regex, target_branch)
4149 if match:
4150 remote_branch = target_branch.replace(match.group(0), replacement)
4151 break
4152 if not match:
4153 # This is a branch path but not one we recognize; use as-is.
4154 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004155 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4156 # Handle the refs that need to land in different refs.
4157 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004158
wittman@chromium.org455dc922015-01-26 20:15:50 +00004159 # Create the true path to the remote branch.
4160 # Does the following translation:
4161 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4162 # * refs/remotes/origin/master -> refs/heads/master
4163 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4164 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4165 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4166 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4167 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4168 'refs/heads/')
4169 elif remote_branch.startswith('refs/remotes/branch-heads'):
4170 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004171
4172 if pending_prefix_check:
4173 # If a pending prefix exists then replace refs/ with it.
4174 state = _GitNumbererState.load(remote_url, remote_branch)
4175 if state.pending_prefix:
4176 remote_branch = remote_branch.replace('refs/', state.pending_prefix)
wittman@chromium.org455dc922015-01-26 20:15:50 +00004177 return remote_branch
4178
4179
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004180def cleanup_list(l):
4181 """Fixes a list so that comma separated items are put as individual items.
4182
4183 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4184 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4185 """
4186 items = sum((i.split(',') for i in l), [])
4187 stripped_items = (i.strip() for i in items)
4188 return sorted(filter(None, stripped_items))
4189
4190
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004191@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004192def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004193 """Uploads the current changelist to codereview.
4194
4195 Can skip dependency patchset uploads for a branch by running:
4196 git config branch.branch_name.skip-deps-uploads True
4197 To unset run:
4198 git config --unset branch.branch_name.skip-deps-uploads
4199 Can also set the above globally by using the --global flag.
4200 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004201 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4202 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004203 parser.add_option('--bypass-watchlists', action='store_true',
4204 dest='bypass_watchlists',
4205 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004206 parser.add_option('-f', action='store_true', dest='force',
4207 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004208 parser.add_option('--message', '-m', dest='message',
4209 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004210 parser.add_option('-b', '--bug',
4211 help='pre-populate the bug number(s) for this issue. '
4212 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004213 parser.add_option('--message-file', dest='message_file',
4214 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004215 parser.add_option('--title', '-t', dest='title',
4216 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004217 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004218 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004219 help='reviewer email addresses')
4220 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004221 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004222 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004223 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004224 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004225 parser.add_option('--emulate_svn_auto_props',
4226 '--emulate-svn-auto-props',
4227 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004228 dest="emulate_svn_auto_props",
4229 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004230 parser.add_option('-c', '--use-commit-queue', action='store_true',
4231 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004232 parser.add_option('--private', action='store_true',
4233 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004234 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004235 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004236 metavar='TARGET',
4237 help='Apply CL to remote ref TARGET. ' +
4238 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004239 parser.add_option('--squash', action='store_true',
4240 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004241 parser.add_option('--no-squash', action='store_true',
4242 help='Don\'t squash multiple commits into one ' +
4243 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004244 parser.add_option('--topic', default=None,
4245 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004246 parser.add_option('--email', default=None,
4247 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004248 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4249 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004250 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4251 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004252 help='Send the patchset to do a CQ dry run right after '
4253 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004254 parser.add_option('--dependencies', action='store_true',
4255 help='Uploads CLs of all the local branches that depend on '
4256 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004257
rmistry@google.com2dd99862015-06-22 12:22:18 +00004258 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004259 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004260 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004261 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004262 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004263 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004264 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004265
sbc@chromium.org71437c02015-04-09 19:29:40 +00004266 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004267 return 1
4268
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004269 options.reviewers = cleanup_list(options.reviewers)
4270 options.cc = cleanup_list(options.cc)
4271
tandriib80458a2016-06-23 12:20:07 -07004272 if options.message_file:
4273 if options.message:
4274 parser.error('only one of --message and --message-file allowed.')
4275 options.message = gclient_utils.FileRead(options.message_file)
4276 options.message_file = None
4277
tandrii4d0545a2016-07-06 03:56:49 -07004278 if options.cq_dry_run and options.use_commit_queue:
4279 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4280
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004281 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4282 settings.GetIsGerrit()
4283
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004284 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004285 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004286
4287
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004288def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004289 print()
4290 print('Waiting for commit to be landed on %s...' % real_ref)
4291 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004292 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4293 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004294 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004295
4296 loop = 0
4297 while True:
4298 sys.stdout.write('fetching (%d)... \r' % loop)
4299 sys.stdout.flush()
4300 loop += 1
4301
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004302 if mirror:
4303 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004304 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4305 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4306 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4307 for commit in commits.splitlines():
4308 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004309 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004310 return commit
4311
4312 current_rev = to_rev
4313
4314
tandriibf429402016-09-14 07:09:12 -07004315def PushToGitPending(remote, pending_ref):
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004316 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4317
4318 Returns:
4319 (retcode of last operation, output log of last operation).
4320 """
4321 assert pending_ref.startswith('refs/'), pending_ref
4322 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4323 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4324 code = 0
4325 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004326 max_attempts = 3
4327 attempts_left = max_attempts
4328 while attempts_left:
4329 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004330 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004331 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004332
4333 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004334 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004335 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004336 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004337 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004338 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004339 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004340 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004341 continue
4342
4343 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004344 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004345 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004346 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004347 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004348 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4349 'the following files have merge conflicts:' % pending_ref)
4350 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4351 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004352 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004353 return code, out
4354
4355 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004356 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004357 code, out = RunGitWithCode(
4358 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4359 if code == 0:
4360 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004361 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004362 return code, out
4363
vapiera7fbd5a2016-06-16 09:17:49 -07004364 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004365 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004366 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004367 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004368 print('Fatal push error. Make sure your .netrc credentials and git '
4369 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004370 return code, out
4371
vapiera7fbd5a2016-06-16 09:17:49 -07004372 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004373 return code, out
4374
4375
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004376def IsFatalPushFailure(push_stdout):
4377 """True if retrying push won't help."""
4378 return '(prohibited by Gerrit)' in push_stdout
4379
4380
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004381@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004382def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004383 """DEPRECATED: Used to commit the current changelist via git-svn."""
4384 message = ('git-cl no longer supports committing to SVN repositories via '
4385 'git-svn. You probably want to use `git cl land` instead.')
4386 print(message)
4387 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004388
4389
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004390@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004391def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004392 """Commits the current changelist via git.
4393
4394 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4395 upstream and closes the issue automatically and atomically.
4396
4397 Otherwise (in case of Rietveld):
4398 Squashes branch into a single commit.
4399 Updates commit message with metadata (e.g. pointer to review).
4400 Pushes the code upstream.
4401 Updates review and closes.
4402 """
4403 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4404 help='bypass upload presubmit hook')
4405 parser.add_option('-m', dest='message',
4406 help="override review description")
4407 parser.add_option('-f', action='store_true', dest='force',
4408 help="force yes to questions (don't prompt)")
4409 parser.add_option('-c', dest='contributor',
4410 help="external contributor for patch (appended to " +
4411 "description and used as author for git). Should be " +
4412 "formatted as 'First Last <email@example.com>'")
4413 add_git_similarity(parser)
4414 auth.add_auth_options(parser)
4415 (options, args) = parser.parse_args(args)
4416 auth_config = auth.extract_auth_config_from_options(options)
4417
4418 cl = Changelist(auth_config=auth_config)
4419
4420 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4421 if cl.IsGerrit():
4422 if options.message:
4423 # This could be implemented, but it requires sending a new patch to
4424 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4425 # Besides, Gerrit has the ability to change the commit message on submit
4426 # automatically, thus there is no need to support this option (so far?).
4427 parser.error('-m MESSAGE option is not supported for Gerrit.')
4428 if options.contributor:
4429 parser.error(
4430 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4431 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4432 'the contributor\'s "name <email>". If you can\'t upload such a '
4433 'commit for review, contact your repository admin and request'
4434 '"Forge-Author" permission.')
4435 if not cl.GetIssue():
4436 DieWithError('You must upload the change first to Gerrit.\n'
4437 ' If you would rather have `git cl land` upload '
4438 'automatically for you, see http://crbug.com/642759')
4439 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4440 options.verbose)
4441
4442 current = cl.GetBranch()
4443 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4444 if remote == '.':
4445 print()
4446 print('Attempting to push branch %r into another local branch!' % current)
4447 print()
4448 print('Either reparent this branch on top of origin/master:')
4449 print(' git reparent-branch --root')
4450 print()
4451 print('OR run `git rebase-update` if you think the parent branch is ')
4452 print('already committed.')
4453 print()
4454 print(' Current parent: %r' % upstream_branch)
4455 return 1
4456
4457 if not args:
4458 # Default to merging against our best guess of the upstream branch.
4459 args = [cl.GetUpstreamBranch()]
4460
4461 if options.contributor:
4462 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
4463 print("Please provide contibutor as 'First Last <email@example.com>'")
4464 return 1
4465
4466 base_branch = args[0]
4467
4468 if git_common.is_dirty_git_tree('land'):
4469 return 1
4470
4471 # This rev-list syntax means "show all commits not in my branch that
4472 # are in base_branch".
4473 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4474 base_branch]).splitlines()
4475 if upstream_commits:
4476 print('Base branch "%s" has %d commits '
4477 'not in this branch.' % (base_branch, len(upstream_commits)))
4478 print('Run "git merge %s" before attempting to land.' % base_branch)
4479 return 1
4480
4481 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
4482 if not options.bypass_hooks:
4483 author = None
4484 if options.contributor:
4485 author = re.search(r'\<(.*)\>', options.contributor).group(1)
4486 hook_results = cl.RunHook(
4487 committing=True,
4488 may_prompt=not options.force,
4489 verbose=options.verbose,
4490 change=cl.GetChange(merge_base, author))
4491 if not hook_results.should_continue():
4492 return 1
4493
4494 # Check the tree status if the tree status URL is set.
4495 status = GetTreeStatus()
4496 if 'closed' == status:
4497 print('The tree is closed. Please wait for it to reopen. Use '
4498 '"git cl land --bypass-hooks" to commit on a closed tree.')
4499 return 1
4500 elif 'unknown' == status:
4501 print('Unable to determine tree status. Please verify manually and '
4502 'use "git cl land --bypass-hooks" to commit on a closed tree.')
4503 return 1
4504
4505 change_desc = ChangeDescription(options.message)
4506 if not change_desc.description and cl.GetIssue():
4507 change_desc = ChangeDescription(cl.GetDescription())
4508
4509 if not change_desc.description:
4510 if not cl.GetIssue() and options.bypass_hooks:
4511 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
4512 else:
4513 print('No description set.')
4514 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
4515 return 1
4516
4517 # Keep a separate copy for the commit message, because the commit message
4518 # contains the link to the Rietveld issue, while the Rietveld message contains
4519 # the commit viewvc url.
4520 if cl.GetIssue():
4521 change_desc.update_reviewers(cl.GetApprovingReviewers())
4522
4523 commit_desc = ChangeDescription(change_desc.description)
4524 if cl.GetIssue():
4525 # Xcode won't linkify this URL unless there is a non-whitespace character
4526 # after it. Add a period on a new line to circumvent this. Also add a space
4527 # before the period to make sure that Gitiles continues to correctly resolve
4528 # the URL.
4529 commit_desc.append_footer('Review-Url: %s .' % cl.GetIssueURL())
4530 if options.contributor:
4531 commit_desc.append_footer('Patch from %s.' % options.contributor)
4532
4533 print('Description:')
4534 print(commit_desc.description)
4535
4536 branches = [merge_base, cl.GetBranchRef()]
4537 if not options.force:
4538 print_stats(options.similarity, options.find_copies, branches)
4539
4540 # We want to squash all this branch's commits into one commit with the proper
4541 # description. We do this by doing a "reset --soft" to the base branch (which
4542 # keeps the working copy the same), then landing that.
4543 MERGE_BRANCH = 'git-cl-commit'
4544 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4545 # Delete the branches if they exist.
4546 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4547 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4548 result = RunGitWithCode(showref_cmd)
4549 if result[0] == 0:
4550 RunGit(['branch', '-D', branch])
4551
4552 # We might be in a directory that's present in this branch but not in the
4553 # trunk. Move up to the top of the tree so that git commands that expect a
4554 # valid CWD won't fail after we check out the merge branch.
4555 rel_base_path = settings.GetRelativeRoot()
4556 if rel_base_path:
4557 os.chdir(rel_base_path)
4558
4559 # Stuff our change into the merge branch.
4560 # We wrap in a try...finally block so if anything goes wrong,
4561 # we clean up the branches.
4562 retcode = -1
4563 pushed_to_pending = False
4564 pending_ref = None
4565 revision = None
4566 try:
4567 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
4568 RunGit(['reset', '--soft', merge_base])
4569 if options.contributor:
4570 RunGit(
4571 [
4572 'commit', '--author', options.contributor,
4573 '-m', commit_desc.description,
4574 ])
4575 else:
4576 RunGit(['commit', '-m', commit_desc.description])
4577
4578 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
4579 mirror = settings.GetGitMirror(remote)
4580 if mirror:
4581 pushurl = mirror.url
4582 git_numberer = _GitNumbererState.load(pushurl, branch)
4583 else:
4584 pushurl = remote # Usually, this is 'origin'.
4585 git_numberer = _GitNumbererState.load(
4586 RunGit(['config', 'remote.%s.url' % remote]).strip(), branch)
4587
4588 if git_numberer.should_add_git_number:
4589 # TODO(tandrii): run git fetch in a loop + autorebase when there there
4590 # is no pending ref to push to?
4591 logging.debug('Adding git number footers')
4592 parent_msg = RunGit(['show', '-s', '--format=%B', merge_base]).strip()
4593 commit_desc.update_with_git_number_footers(merge_base, parent_msg,
4594 branch)
4595 # Ensure timestamps are monotonically increasing.
4596 timestamp = max(1 + _get_committer_timestamp(merge_base),
4597 _get_committer_timestamp('HEAD'))
4598 _git_amend_head(commit_desc.description, timestamp)
4599 change_desc = ChangeDescription(commit_desc.description)
4600 # If gnumbd is sitll ON and we ultimately push to branch with
4601 # pending_prefix, gnumbd will modify footers we've just inserted with
4602 # 'Original-', which is annoying but still technically correct.
4603
4604 pending_prefix = git_numberer.pending_prefix
4605 if not pending_prefix or branch.startswith(pending_prefix):
4606 # If not using refs/pending/heads/* at all, or target ref is already set
4607 # to pending, then push to the target ref directly.
4608 # NB(tandrii): I think branch.startswith(pending_prefix) never happens
4609 # in practise. I really tried to create a new branch tracking
4610 # refs/pending/heads/master directly and git cl land failed long before
4611 # reaching this. Disagree? Comment on http://crbug.com/642493.
4612 if pending_prefix:
4613 print('\n\nYOU GOT A CHANCE TO WIN A FREE GIFT!\n\n'
4614 'Grab your .git/config, add instructions how to reproduce '
4615 'this, and post it to http://crbug.com/642493.\n'
4616 'The first reporter gets a free "Black Swan" book from '
4617 'tandrii@\n\n')
4618 retcode, output = RunGitWithCode(
4619 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
4620 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
4621 else:
4622 # Cherry-pick the change on top of pending ref and then push it.
4623 assert branch.startswith('refs/'), branch
4624 assert pending_prefix[-1] == '/', pending_prefix
4625 pending_ref = pending_prefix + branch[len('refs/'):]
4626 retcode, output = PushToGitPending(pushurl, pending_ref)
4627 pushed_to_pending = (retcode == 0)
4628
4629 if retcode == 0:
4630 revision = RunGit(['rev-parse', 'HEAD']).strip()
4631 logging.debug(output)
4632 except: # pylint: disable=bare-except
4633 if _IS_BEING_TESTED:
4634 logging.exception('this is likely your ACTUAL cause of test failure.\n'
4635 + '-' * 30 + '8<' + '-' * 30)
4636 logging.error('\n' + '-' * 30 + '8<' + '-' * 30 + '\n\n\n')
4637 raise
4638 finally:
4639 # And then swap back to the original branch and clean up.
4640 RunGit(['checkout', '-q', cl.GetBranch()])
4641 RunGit(['branch', '-D', MERGE_BRANCH])
4642
4643 if not revision:
4644 print('Failed to push. If this persists, please file a bug.')
4645 return 1
4646
4647 killed = False
4648 if pushed_to_pending:
4649 try:
4650 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4651 # We set pushed_to_pending to False, since it made it all the way to the
4652 # real ref.
4653 pushed_to_pending = False
4654 except KeyboardInterrupt:
4655 killed = True
4656
4657 if cl.GetIssue():
4658 to_pending = ' to pending queue' if pushed_to_pending else ''
4659 viewvc_url = settings.GetViewVCUrl()
4660 if not to_pending:
4661 if viewvc_url and revision:
4662 change_desc.append_footer(
4663 'Committed: %s%s' % (viewvc_url, revision))
4664 elif revision:
4665 change_desc.append_footer('Committed: %s' % (revision,))
4666 print('Closing issue '
4667 '(you may be prompted for your codereview password)...')
4668 cl.UpdateDescription(change_desc.description)
4669 cl.CloseIssue()
4670 props = cl.GetIssueProperties()
4671 patch_num = len(props['patchsets'])
4672 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
4673 patch_num, props['patchsets'][-1], to_pending, revision)
4674 if options.bypass_hooks:
4675 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4676 else:
4677 comment += ' (presubmit successful).'
4678 cl.RpcServer().add_comment(cl.GetIssue(), comment)
4679
4680 if pushed_to_pending:
4681 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
4682 print('The commit is in the pending queue (%s).' % pending_ref)
4683 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4684 'footer.' % branch)
4685
4686 if os.path.isfile(POSTUPSTREAM_HOOK):
4687 RunCommand([POSTUPSTREAM_HOOK, merge_base], error_ok=True)
4688
4689 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004690
4691
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004692@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004693def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004694 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004695 parser.add_option('-b', dest='newbranch',
4696 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004697 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004698 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004699 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4700 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004701 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004702 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004703 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004704 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004705 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004706 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004707
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004708
4709 group = optparse.OptionGroup(
4710 parser,
4711 'Options for continuing work on the current issue uploaded from a '
4712 'different clone (e.g. different machine). Must be used independently '
4713 'from the other options. No issue number should be specified, and the '
4714 'branch must have an issue number associated with it')
4715 group.add_option('--reapply', action='store_true', dest='reapply',
4716 help='Reset the branch and reapply the issue.\n'
4717 'CAUTION: This will undo any local changes in this '
4718 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004719
4720 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004721 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004722 parser.add_option_group(group)
4723
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004724 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004725 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004726 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004727 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004728 auth_config = auth.extract_auth_config_from_options(options)
4729
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004730
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004731 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004732 if options.newbranch:
4733 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004734 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004735 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004736
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004737 cl = Changelist(auth_config=auth_config,
4738 codereview=options.forced_codereview)
4739 if not cl.GetIssue():
4740 parser.error('current branch must have an associated issue')
4741
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004742 upstream = cl.GetUpstreamBranch()
4743 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004744 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004745
4746 RunGit(['reset', '--hard', upstream])
4747 if options.pull:
4748 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004749
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004750 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4751 options.directory)
4752
4753 if len(args) != 1 or not args[0]:
4754 parser.error('Must specify issue number or url')
4755
4756 # We don't want uncommitted changes mixed up with the patch.
4757 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004758 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004759
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004760 if options.newbranch:
4761 if options.force:
4762 RunGit(['branch', '-D', options.newbranch],
4763 stderr=subprocess2.PIPE, error_ok=True)
4764 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004765 elif not GetCurrentBranch():
4766 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004767
4768 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4769
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004770 if cl.IsGerrit():
4771 if options.reject:
4772 parser.error('--reject is not supported with Gerrit codereview.')
4773 if options.nocommit:
4774 parser.error('--nocommit is not supported with Gerrit codereview.')
4775 if options.directory:
4776 parser.error('--directory is not supported with Gerrit codereview.')
4777
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004778 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004779 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004780
4781
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004782def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004783 """Fetches the tree status and returns either 'open', 'closed',
4784 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004785 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004786 if url:
4787 status = urllib2.urlopen(url).read().lower()
4788 if status.find('closed') != -1 or status == '0':
4789 return 'closed'
4790 elif status.find('open') != -1 or status == '1':
4791 return 'open'
4792 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004793 return 'unset'
4794
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004795
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004796def GetTreeStatusReason():
4797 """Fetches the tree status from a json url and returns the message
4798 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004799 url = settings.GetTreeStatusUrl()
4800 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004801 connection = urllib2.urlopen(json_url)
4802 status = json.loads(connection.read())
4803 connection.close()
4804 return status['message']
4805
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004806
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004807def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004808 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004809 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004810 status = GetTreeStatus()
4811 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004812 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004813 return 2
4814
vapiera7fbd5a2016-06-16 09:17:49 -07004815 print('The tree is %s' % status)
4816 print()
4817 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004818 if status != 'open':
4819 return 1
4820 return 0
4821
4822
maruel@chromium.org15192402012-09-06 12:38:29 +00004823def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004824 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004825 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004826 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004827 '-b', '--bot', action='append',
4828 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4829 'times to specify multiple builders. ex: '
4830 '"-b win_rel -b win_layout". See '
4831 'the try server waterfall for the builders name and the tests '
4832 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004833 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004834 '-B', '--bucket', default='',
4835 help=('Buildbucket bucket to send the try requests.'))
4836 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004837 '-m', '--master', default='',
4838 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004839 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004840 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004841 help='Revision to use for the try job; default: the revision will '
4842 'be determined by the try recipe that builder runs, which usually '
4843 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004844 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004845 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004846 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004847 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004848 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004849 '--project',
4850 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004851 'in recipe to determine to which repository or directory to '
4852 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004853 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004854 '-p', '--property', dest='properties', action='append', default=[],
4855 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004856 'key2=value2 etc. The value will be treated as '
4857 'json if decodable, or as string otherwise. '
4858 'NOTE: using this may make your try job not usable for CQ, '
4859 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004860 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004861 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4862 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004863 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004864 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004865 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004866 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004867
machenbach@chromium.org45453142015-09-15 08:45:22 +00004868 # Make sure that all properties are prop=value pairs.
4869 bad_params = [x for x in options.properties if '=' not in x]
4870 if bad_params:
4871 parser.error('Got properties with missing "=": %s' % bad_params)
4872
maruel@chromium.org15192402012-09-06 12:38:29 +00004873 if args:
4874 parser.error('Unknown arguments: %s' % args)
4875
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004876 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004877 if not cl.GetIssue():
4878 parser.error('Need to upload first')
4879
tandriie113dfd2016-10-11 10:20:12 -07004880 error_message = cl.CannotTriggerTryJobReason()
4881 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004882 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004883
borenet6c0efe62016-10-19 08:13:29 -07004884 if options.bucket and options.master:
4885 parser.error('Only one of --bucket and --master may be used.')
4886
qyearsley1fdfcb62016-10-24 13:22:03 -07004887 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004888
qyearsleydd49f942016-10-28 11:57:22 -07004889 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4890 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004891 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004892 if options.verbose:
4893 print('git cl try with no bots now defaults to CQ Dry Run.')
4894 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004895
borenet6c0efe62016-10-19 08:13:29 -07004896 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004897 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004898 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004899 'of bot requires an initial job from a parent (usually a builder). '
4900 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004901 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004902 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004903
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004904 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004905 # TODO(tandrii): Checking local patchset against remote patchset is only
4906 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4907 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07004908 print('Warning: Codereview server has newer patchsets (%s) than most '
4909 'recent upload from local checkout (%s). Did a previous upload '
4910 'fail?\n'
4911 'By default, git cl try uses the latest patchset from '
4912 'codereview, continuing to use patchset %s.\n' %
4913 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07004914
tandrii568043b2016-10-11 07:49:18 -07004915 try:
borenet6c0efe62016-10-19 08:13:29 -07004916 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
4917 patchset)
tandrii568043b2016-10-11 07:49:18 -07004918 except BuildbucketResponseException as ex:
4919 print('ERROR: %s' % ex)
4920 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004921 return 0
4922
4923
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004924def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004925 """Prints info about try jobs associated with current CL."""
4926 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004927 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004928 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004929 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004930 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004931 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004932 '--color', action='store_true', default=setup_color.IS_TTY,
4933 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004934 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004935 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4936 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004937 group.add_option(
4938 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004939 parser.add_option_group(group)
4940 auth.add_auth_options(parser)
4941 options, args = parser.parse_args(args)
4942 if args:
4943 parser.error('Unrecognized args: %s' % ' '.join(args))
4944
4945 auth_config = auth.extract_auth_config_from_options(options)
4946 cl = Changelist(auth_config=auth_config)
4947 if not cl.GetIssue():
4948 parser.error('Need to upload first')
4949
tandrii221ab252016-10-06 08:12:04 -07004950 patchset = options.patchset
4951 if not patchset:
4952 patchset = cl.GetMostRecentPatchset()
4953 if not patchset:
4954 parser.error('Codereview doesn\'t know about issue %s. '
4955 'No access to issue or wrong issue number?\n'
4956 'Either upload first, or pass --patchset explicitely' %
4957 cl.GetIssue())
4958
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004959 # TODO(tandrii): Checking local patchset against remote patchset is only
4960 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4961 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07004962 print('Warning: Codereview server has newer patchsets (%s) than most '
4963 'recent upload from local checkout (%s). Did a previous upload '
4964 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07004965 'By default, git cl try-results uses the latest patchset from '
4966 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07004967 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004968 try:
tandrii221ab252016-10-06 08:12:04 -07004969 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004970 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004971 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004972 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004973 if options.json:
4974 write_try_results_json(options.json, jobs)
4975 else:
4976 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004977 return 0
4978
4979
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004980@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004981def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004982 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004983 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004984 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004985 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004986
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004987 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004988 if args:
4989 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004990 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004991 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004992 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004993 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004994
4995 # Clear configured merge-base, if there is one.
4996 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004997 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004998 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004999 return 0
5000
5001
thestig@chromium.org00858c82013-12-02 23:08:03 +00005002def CMDweb(parser, args):
5003 """Opens the current CL in the web browser."""
5004 _, args = parser.parse_args(args)
5005 if args:
5006 parser.error('Unrecognized args: %s' % ' '.join(args))
5007
5008 issue_url = Changelist().GetIssueURL()
5009 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005010 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005011 return 1
5012
5013 webbrowser.open(issue_url)
5014 return 0
5015
5016
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005017def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005018 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005019 parser.add_option('-d', '--dry-run', action='store_true',
5020 help='trigger in dry run mode')
5021 parser.add_option('-c', '--clear', action='store_true',
5022 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005023 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005024 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005025 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005026 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005027 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005028 if args:
5029 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005030 if options.dry_run and options.clear:
5031 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5032
iannuccie53c9352016-08-17 14:40:40 -07005033 cl = Changelist(auth_config=auth_config, issue=options.issue,
5034 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005035 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005036 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005037 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07005038 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005039 state = _CQState.DRY_RUN
5040 else:
5041 state = _CQState.COMMIT
5042 if not cl.GetIssue():
5043 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005044 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005045 return 0
5046
5047
groby@chromium.org411034a2013-02-26 15:12:01 +00005048def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005049 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005050 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005051 auth.add_auth_options(parser)
5052 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005053 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005054 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005055 if args:
5056 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005057 cl = Changelist(auth_config=auth_config, issue=options.issue,
5058 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005059 # Ensure there actually is an issue to close.
5060 cl.GetDescription()
5061 cl.CloseIssue()
5062 return 0
5063
5064
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005065def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005066 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005067 parser.add_option(
5068 '--stat',
5069 action='store_true',
5070 dest='stat',
5071 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005072 auth.add_auth_options(parser)
5073 options, args = parser.parse_args(args)
5074 auth_config = auth.extract_auth_config_from_options(options)
5075 if args:
5076 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005077
5078 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005079 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005080 # Staged changes would be committed along with the patch from last
5081 # upload, hence counted toward the "last upload" side in the final
5082 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005083 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005084 return 1
5085
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005086 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005087 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005088 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005089 if not issue:
5090 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005091 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005092 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005093
5094 # Create a new branch based on the merge-base
5095 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005096 # Clear cached branch in cl object, to avoid overwriting original CL branch
5097 # properties.
5098 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005099 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005100 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005101 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005102 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005103 return rtn
5104
wychen@chromium.org06928532015-02-03 02:11:29 +00005105 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005106 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005107 cmd = ['git', 'diff']
5108 if options.stat:
5109 cmd.append('--stat')
5110 cmd.extend([TMP_BRANCH, branch, '--'])
5111 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005112 finally:
5113 RunGit(['checkout', '-q', branch])
5114 RunGit(['branch', '-D', TMP_BRANCH])
5115
5116 return 0
5117
5118
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005119def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005120 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005121 parser.add_option(
5122 '--no-color',
5123 action='store_true',
5124 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005125 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005126 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005127 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005128
5129 author = RunGit(['config', 'user.email']).strip() or None
5130
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005131 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005132
5133 if args:
5134 if len(args) > 1:
5135 parser.error('Unknown args')
5136 base_branch = args[0]
5137 else:
5138 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005139 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005140
5141 change = cl.GetChange(base_branch, None)
5142 return owners_finder.OwnersFinder(
5143 [f.LocalPath() for f in
5144 cl.GetChange(base_branch, None).AffectedFiles()],
5145 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005146 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005147 disable_color=options.no_color).run()
5148
5149
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005150def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005151 """Generates a diff command."""
5152 # Generate diff for the current branch's changes.
5153 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
5154 upstream_commit, '--' ]
5155
5156 if args:
5157 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005158 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005159 diff_cmd.append(arg)
5160 else:
5161 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005162
5163 return diff_cmd
5164
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005165def MatchingFileType(file_name, extensions):
5166 """Returns true if the file name ends with one of the given extensions."""
5167 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005168
enne@chromium.org555cfe42014-01-29 18:21:39 +00005169@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005170def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005171 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lam06dba1b2017-01-18 16:39:43 +11005172 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java', '.js']
kylechar58edce22016-06-17 06:07:51 -07005173 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005174 parser.add_option('--full', action='store_true',
5175 help='Reformat the full content of all touched files')
5176 parser.add_option('--dry-run', action='store_true',
5177 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005178 parser.add_option('--python', action='store_true',
5179 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005180 parser.add_option('--diff', action='store_true',
5181 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005182 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005183
Daniel Chengc55eecf2016-12-30 03:11:02 -08005184 # Normalize any remaining args against the current path, so paths relative to
5185 # the current directory are still resolved as expected.
5186 args = [os.path.join(os.getcwd(), arg) for arg in args]
5187
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005188 # git diff generates paths against the root of the repository. Change
5189 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005190 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005191 if rel_base_path:
5192 os.chdir(rel_base_path)
5193
digit@chromium.org29e47272013-05-17 17:01:46 +00005194 # Grab the merge-base commit, i.e. the upstream commit of the current
5195 # branch when it was created or the last time it was rebased. This is
5196 # to cover the case where the user may have called "git fetch origin",
5197 # moving the origin branch to a newer commit, but hasn't rebased yet.
5198 upstream_commit = None
5199 cl = Changelist()
5200 upstream_branch = cl.GetUpstreamBranch()
5201 if upstream_branch:
5202 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5203 upstream_commit = upstream_commit.strip()
5204
5205 if not upstream_commit:
5206 DieWithError('Could not find base commit for this branch. '
5207 'Are you in detached state?')
5208
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005209 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5210 diff_output = RunGit(changed_files_cmd)
5211 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005212 # Filter out files deleted by this CL
5213 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005214
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005215 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5216 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5217 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005218 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005219
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005220 top_dir = os.path.normpath(
5221 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5222
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005223 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5224 # formatted. This is used to block during the presubmit.
5225 return_value = 0
5226
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005227 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005228 # Locate the clang-format binary in the checkout
5229 try:
5230 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005231 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005232 DieWithError(e)
5233
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005234 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005235 cmd = [clang_format_tool]
5236 if not opts.dry_run and not opts.diff:
5237 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005238 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005239 if opts.diff:
5240 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005241 else:
5242 env = os.environ.copy()
5243 env['PATH'] = str(os.path.dirname(clang_format_tool))
5244 try:
5245 script = clang_format.FindClangFormatScriptInChromiumTree(
5246 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005247 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005248 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005249
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005250 cmd = [sys.executable, script, '-p0']
5251 if not opts.dry_run and not opts.diff:
5252 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005253
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005254 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5255 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005256
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005257 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5258 if opts.diff:
5259 sys.stdout.write(stdout)
5260 if opts.dry_run and len(stdout) > 0:
5261 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005262
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005263 # Similar code to above, but using yapf on .py files rather than clang-format
5264 # on C/C++ files
5265 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005266 yapf_tool = gclient_utils.FindExecutable('yapf')
5267 if yapf_tool is None:
5268 DieWithError('yapf not found in PATH')
5269
5270 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005271 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005272 cmd = [yapf_tool]
5273 if not opts.dry_run and not opts.diff:
5274 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005275 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005276 if opts.diff:
5277 sys.stdout.write(stdout)
5278 else:
5279 # TODO(sbc): yapf --lines mode still has some issues.
5280 # https://github.com/google/yapf/issues/154
5281 DieWithError('--python currently only works with --full')
5282
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005283 # Dart's formatter does not have the nice property of only operating on
5284 # modified chunks, so hard code full.
5285 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005286 try:
5287 command = [dart_format.FindDartFmtToolInChromiumTree()]
5288 if not opts.dry_run and not opts.diff:
5289 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005290 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005291
ppi@chromium.org6593d932016-03-03 15:41:15 +00005292 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005293 if opts.dry_run and stdout:
5294 return_value = 2
5295 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005296 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5297 'found in this checkout. Files in other languages are still '
5298 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005299
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005300 # Format GN build files. Always run on full build files for canonical form.
5301 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005302 cmd = ['gn', 'format' ]
5303 if opts.dry_run or opts.diff:
5304 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005305 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005306 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5307 shell=sys.platform == 'win32',
5308 cwd=top_dir)
5309 if opts.dry_run and gn_ret == 2:
5310 return_value = 2 # Not formatted.
5311 elif opts.diff and gn_ret == 2:
5312 # TODO this should compute and print the actual diff.
5313 print("This change has GN build file diff for " + gn_diff_file)
5314 elif gn_ret != 0:
5315 # For non-dry run cases (and non-2 return values for dry-run), a
5316 # nonzero error code indicates a failure, probably because the file
5317 # doesn't parse.
5318 DieWithError("gn format failed on " + gn_diff_file +
5319 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005320
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005321 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005322
5323
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005324@subcommand.usage('<codereview url or issue id>')
5325def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005326 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005327 _, args = parser.parse_args(args)
5328
5329 if len(args) != 1:
5330 parser.print_help()
5331 return 1
5332
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005333 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005334 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005335 parser.print_help()
5336 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005337 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005338
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005339 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005340 output = RunGit(['config', '--local', '--get-regexp',
5341 r'branch\..*\.%s' % issueprefix],
5342 error_ok=True)
5343 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005344 if issue == target_issue:
5345 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005346
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005347 branches = []
5348 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005349 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005350 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005351 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005352 return 1
5353 if len(branches) == 1:
5354 RunGit(['checkout', branches[0]])
5355 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005356 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005357 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005358 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005359 which = raw_input('Choose by index: ')
5360 try:
5361 RunGit(['checkout', branches[int(which)]])
5362 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005363 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005364 return 1
5365
5366 return 0
5367
5368
maruel@chromium.org29404b52014-09-08 22:58:00 +00005369def CMDlol(parser, args):
5370 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005371 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005372 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5373 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5374 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005375 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005376 return 0
5377
5378
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005379class OptionParser(optparse.OptionParser):
5380 """Creates the option parse and add --verbose support."""
5381 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005382 optparse.OptionParser.__init__(
5383 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005384 self.add_option(
5385 '-v', '--verbose', action='count', default=0,
5386 help='Use 2 times for more debugging info')
5387
5388 def parse_args(self, args=None, values=None):
5389 options, args = optparse.OptionParser.parse_args(self, args, values)
5390 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5391 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5392 return options, args
5393
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005394
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005395def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005396 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005397 print('\nYour python version %s is unsupported, please upgrade.\n' %
5398 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005399 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005400
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005401 # Reload settings.
5402 global settings
5403 settings = Settings()
5404
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005405 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005406 dispatcher = subcommand.CommandDispatcher(__name__)
5407 try:
5408 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005409 except auth.AuthenticationError as e:
5410 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005411 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005412 if e.code != 500:
5413 raise
5414 DieWithError(
5415 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5416 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005417 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005418
5419
5420if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005421 # These affect sys.stdout so do it outside of main() to simplify mocks in
5422 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005423 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005424 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005425 try:
5426 sys.exit(main(sys.argv[1:]))
5427 except KeyboardInterrupt:
5428 sys.stderr.write('interrupted\n')
5429 sys.exit(1)