blob: 73d2bf9eb44ada00e34f07a436e1c47b75de91c8 [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'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000069POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000070DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000071GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000072REFS_THAT_ALIAS_TO_OTHER_REFS = {
73 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
74 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
75}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000076
thestig@chromium.org44202a22014-03-11 19:22:18 +000077# Valid extensions for files we want to lint.
78DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
79DEFAULT_LINT_IGNORE_REGEX = r"$^"
80
borenet6c0efe62016-10-19 08:13:29 -070081# Buildbucket master name prefix.
82MASTER_PREFIX = 'master.'
83
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000084# Shortcut since it quickly becomes redundant.
85Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000086
maruel@chromium.orgddd59412011-11-30 14:20:38 +000087# Initialized in main()
88settings = None
89
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010090# Used by tests/git_cl_test.py to add extra logging.
91# Inside the weirdly failing test, add this:
92# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
93# And scroll up to see the strack trace printed.
94_IS_BEING_TESTED = False
95
maruel@chromium.orgddd59412011-11-30 14:20:38 +000096
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000097def DieWithError(message):
vapiera7fbd5a2016-06-16 09:17:49 -070098 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000099 sys.exit(1)
100
101
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000102def GetNoGitPagerEnv():
103 env = os.environ.copy()
104 # 'cat' is a magical git string that disables pagers on all platforms.
105 env['GIT_PAGER'] = 'cat'
106 return env
107
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000108
bsep@chromium.org627d9002016-04-29 00:00:52 +0000109def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000110 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000111 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000112 except subprocess2.CalledProcessError as e:
113 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000114 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000115 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000116 'Command "%s" failed.\n%s' % (
117 ' '.join(args), error_message or e.stdout or ''))
118 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000119
120
121def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000122 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000123 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000124
125
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000126def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000127 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700128 if suppress_stderr:
129 stderr = subprocess2.VOID
130 else:
131 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000132 try:
tandrii5d48c322016-08-18 16:19:37 -0700133 (out, _), code = subprocess2.communicate(['git'] + args,
134 env=GetNoGitPagerEnv(),
135 stdout=subprocess2.PIPE,
136 stderr=stderr)
137 return code, out
138 except subprocess2.CalledProcessError as e:
139 logging.debug('Failed running %s', args)
140 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000141
142
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000143def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000144 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000145 return RunGitWithCode(args, suppress_stderr=True)[1]
146
147
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000148def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000149 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000150 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000151 return (version.startswith(prefix) and
152 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000153
154
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000155def BranchExists(branch):
156 """Return True if specified branch exists."""
157 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
158 suppress_stderr=True)
159 return not code
160
161
tandrii2a16b952016-10-19 07:09:44 -0700162def time_sleep(seconds):
163 # Use this so that it can be mocked in tests without interfering with python
164 # system machinery.
165 import time # Local import to discourage others from importing time globally.
166 return time.sleep(seconds)
167
168
maruel@chromium.org90541732011-04-01 17:54:18 +0000169def ask_for_data(prompt):
170 try:
171 return raw_input(prompt)
172 except KeyboardInterrupt:
173 # Hide the exception.
174 sys.exit(1)
175
176
tandrii5d48c322016-08-18 16:19:37 -0700177def _git_branch_config_key(branch, key):
178 """Helper method to return Git config key for a branch."""
179 assert branch, 'branch name is required to set git config for it'
180 return 'branch.%s.%s' % (branch, key)
181
182
183def _git_get_branch_config_value(key, default=None, value_type=str,
184 branch=False):
185 """Returns git config value of given or current branch if any.
186
187 Returns default in all other cases.
188 """
189 assert value_type in (int, str, bool)
190 if branch is False: # Distinguishing default arg value from None.
191 branch = GetCurrentBranch()
192
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000193 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700194 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000195
tandrii5d48c322016-08-18 16:19:37 -0700196 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700197 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700198 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700199 # git config also has --int, but apparently git config suffers from integer
200 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700201 args.append(_git_branch_config_key(branch, key))
202 code, out = RunGitWithCode(args)
203 if code == 0:
204 value = out.strip()
205 if value_type == int:
206 return int(value)
207 if value_type == bool:
208 return bool(value.lower() == 'true')
209 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000210 return default
211
212
tandrii5d48c322016-08-18 16:19:37 -0700213def _git_set_branch_config_value(key, value, branch=None, **kwargs):
214 """Sets the value or unsets if it's None of a git branch config.
215
216 Valid, though not necessarily existing, branch must be provided,
217 otherwise currently checked out branch is used.
218 """
219 if not branch:
220 branch = GetCurrentBranch()
221 assert branch, 'a branch name OR currently checked out branch is required'
222 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700223 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700224 if value is None:
225 args.append('--unset')
226 elif isinstance(value, bool):
227 args.append('--bool')
228 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700229 else:
tandrii33a46ff2016-08-23 05:53:40 -0700230 # git config also has --int, but apparently git config suffers from integer
231 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700232 value = str(value)
233 args.append(_git_branch_config_key(branch, key))
234 if value is not None:
235 args.append(value)
236 RunGit(args, **kwargs)
237
238
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100239def _get_committer_timestamp(commit):
240 """Returns unix timestamp as integer of a committer in a commit.
241
242 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
243 """
244 # Git also stores timezone offset, but it only affects visual display,
245 # actual point in time is defined by this timestamp only.
246 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
247
248
249def _git_amend_head(message, committer_timestamp):
250 """Amends commit with new message and desired committer_timestamp.
251
252 Sets committer timezone to UTC.
253 """
254 env = os.environ.copy()
255 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
256 return RunGit(['commit', '--amend', '-m', message], env=env)
257
258
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000259def add_git_similarity(parser):
260 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700261 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000262 help='Sets the percentage that a pair of files need to match in order to'
263 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000264 parser.add_option(
265 '--find-copies', action='store_true',
266 help='Allows git to look for copies.')
267 parser.add_option(
268 '--no-find-copies', action='store_false', dest='find_copies',
269 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000270
271 old_parser_args = parser.parse_args
272 def Parse(args):
273 options, args = old_parser_args(args)
274
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000275 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700276 options.similarity = _git_get_branch_config_value(
277 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000278 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000279 print('Note: Saving similarity of %d%% in git config.'
280 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700281 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000282
iannucci@chromium.org79540052012-10-19 23:15:26 +0000283 options.similarity = max(0, min(options.similarity, 100))
284
285 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700286 options.find_copies = _git_get_branch_config_value(
287 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000288 else:
tandrii5d48c322016-08-18 16:19:37 -0700289 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000290
291 print('Using %d%% similarity for rename/copy detection. '
292 'Override with --similarity.' % options.similarity)
293
294 return options, args
295 parser.parse_args = Parse
296
297
machenbach@chromium.org45453142015-09-15 08:45:22 +0000298def _get_properties_from_options(options):
299 properties = dict(x.split('=', 1) for x in options.properties)
300 for key, val in properties.iteritems():
301 try:
302 properties[key] = json.loads(val)
303 except ValueError:
304 pass # If a value couldn't be evaluated, treat it as a string.
305 return properties
306
307
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000308def _prefix_master(master):
309 """Convert user-specified master name to full master name.
310
311 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
312 name, while the developers always use shortened master name
313 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
314 function does the conversion for buildbucket migration.
315 """
borenet6c0efe62016-10-19 08:13:29 -0700316 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000317 return master
borenet6c0efe62016-10-19 08:13:29 -0700318 return '%s%s' % (MASTER_PREFIX, master)
319
320
321def _unprefix_master(bucket):
322 """Convert bucket name to shortened master name.
323
324 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
325 name, while the developers always use shortened master name
326 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
327 function does the conversion for buildbucket migration.
328 """
329 if bucket.startswith(MASTER_PREFIX):
330 return bucket[len(MASTER_PREFIX):]
331 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000332
333
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000334def _buildbucket_retry(operation_name, http, *args, **kwargs):
335 """Retries requests to buildbucket service and returns parsed json content."""
336 try_count = 0
337 while True:
338 response, content = http.request(*args, **kwargs)
339 try:
340 content_json = json.loads(content)
341 except ValueError:
342 content_json = None
343
344 # Buildbucket could return an error even if status==200.
345 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000346 error = content_json.get('error')
347 if error.get('code') == 403:
348 raise BuildbucketResponseException(
349 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000350 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000351 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000352 raise BuildbucketResponseException(msg)
353
354 if response.status == 200:
355 if not content_json:
356 raise BuildbucketResponseException(
357 'Buildbucket returns invalid json content: %s.\n'
358 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
359 content)
360 return content_json
361 if response.status < 500 or try_count >= 2:
362 raise httplib2.HttpLib2Error(content)
363
364 # status >= 500 means transient failures.
365 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700366 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000367 try_count += 1
368 assert False, 'unreachable'
369
370
qyearsley1fdfcb62016-10-24 13:22:03 -0700371def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700372 """Returns a dict mapping bucket names to builders and tests,
373 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700374 """
qyearsleydd49f942016-10-28 11:57:22 -0700375 # If no bots are listed, we try to get a set of builders and tests based
376 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700377 if not options.bot:
378 change = changelist.GetChange(
379 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700380 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700381 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700382 change=change,
383 changed_files=change.LocalPaths(),
384 repository_root=settings.GetRoot(),
385 default_presubmit=None,
386 project=None,
387 verbose=options.verbose,
388 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700389 if masters is None:
390 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100391 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700392
qyearsley1fdfcb62016-10-24 13:22:03 -0700393 if options.bucket:
394 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700395 if options.master:
396 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700397
qyearsleydd49f942016-10-28 11:57:22 -0700398 # If bots are listed but no master or bucket, then we need to find out
399 # the corresponding master for each bot.
400 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
401 if error_message:
402 option_parser.error(
403 'Tryserver master cannot be found because: %s\n'
404 'Please manually specify the tryserver master, e.g. '
405 '"-m tryserver.chromium.linux".' % error_message)
406 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700407
408
qyearsley123a4682016-10-26 09:12:17 -0700409def _get_bucket_map_for_builders(builders):
410 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700411 map_url = 'https://builders-map.appspot.com/'
412 try:
qyearsley123a4682016-10-26 09:12:17 -0700413 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700414 except urllib2.URLError as e:
415 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
416 (map_url, e))
417 except ValueError as e:
418 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700419 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700420 return None, 'Failed to build master map.'
421
qyearsley123a4682016-10-26 09:12:17 -0700422 bucket_map = {}
423 for builder in builders:
qyearsley123a4682016-10-26 09:12:17 -0700424 masters = builders_map.get(builder, [])
425 if not masters:
qyearsley1fdfcb62016-10-24 13:22:03 -0700426 return None, ('No matching master for builder %s.' % builder)
qyearsley123a4682016-10-26 09:12:17 -0700427 if len(masters) > 1:
qyearsley1fdfcb62016-10-24 13:22:03 -0700428 return None, ('The builder name %s exists in multiple masters %s.' %
qyearsley123a4682016-10-26 09:12:17 -0700429 (builder, masters))
430 bucket = _prefix_master(masters[0])
431 bucket_map.setdefault(bucket, {})[builder] = []
432
433 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700434
435
borenet6c0efe62016-10-19 08:13:29 -0700436def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700437 category='git_cl_try', patchset=None):
qyearsley1fdfcb62016-10-24 13:22:03 -0700438 """Sends a request to Buildbucket to trigger try jobs for a changelist.
439
440 Args:
441 auth_config: AuthConfig for Rietveld.
442 changelist: Changelist that the try jobs are associated with.
443 buckets: A nested dict mapping bucket names to builders to tests.
444 options: Command-line options.
445 """
tandriide281ae2016-10-12 06:02:30 -0700446 assert changelist.GetIssue(), 'CL must be uploaded first'
447 codereview_url = changelist.GetCodereviewServer()
448 assert codereview_url, 'CL must be uploaded first'
449 patchset = patchset or changelist.GetMostRecentPatchset()
450 assert patchset, 'CL must be uploaded first'
451
452 codereview_host = urlparse.urlparse(codereview_url).hostname
453 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000454 http = authenticator.authorize(httplib2.Http())
455 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700456
457 # TODO(tandrii): consider caching Gerrit CL details just like
458 # _RietveldChangelistImpl does, then caching values in these two variables
459 # won't be necessary.
460 owner_email = changelist.GetIssueOwner()
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000461
462 buildbucket_put_url = (
463 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000464 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700465 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
466 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
467 hostname=codereview_host,
468 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000469 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700470
471 shared_parameters_properties = changelist.GetTryjobProperties(patchset)
472 shared_parameters_properties['category'] = category
473 if options.clobber:
474 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700475 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700476 if extra_properties:
477 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000478
479 batch_req_body = {'builds': []}
480 print_text = []
481 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700482 for bucket, builders_and_tests in sorted(buckets.iteritems()):
483 print_text.append('Bucket: %s' % bucket)
484 master = None
485 if bucket.startswith(MASTER_PREFIX):
486 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000487 for builder, tests in sorted(builders_and_tests.iteritems()):
488 print_text.append(' %s: %s' % (builder, tests))
489 parameters = {
490 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000491 'changes': [{
tandriide281ae2016-10-12 06:02:30 -0700492 'author': {'email': owner_email},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000493 'revision': options.revision,
494 }],
tandrii8c5a3532016-11-04 07:52:02 -0700495 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000496 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000497 if 'presubmit' in builder.lower():
498 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000499 if tests:
500 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700501
502 tags = [
503 'builder:%s' % builder,
504 'buildset:%s' % buildset,
505 'user_agent:git_cl_try',
506 ]
507 if master:
508 parameters['properties']['master'] = master
509 tags.append('master:%s' % master)
510
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000511 batch_req_body['builds'].append(
512 {
513 'bucket': bucket,
514 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000515 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700516 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000517 }
518 )
519
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000520 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700521 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000522 http,
523 buildbucket_put_url,
524 'PUT',
525 body=json.dumps(batch_req_body),
526 headers={'Content-Type': 'application/json'}
527 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000528 print_text.append('To see results here, run: git cl try-results')
529 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700530 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000531
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000532
tandrii221ab252016-10-06 08:12:04 -0700533def fetch_try_jobs(auth_config, changelist, buildbucket_host,
534 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700535 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000536
qyearsley53f48a12016-09-01 10:45:13 -0700537 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000538 """
tandrii221ab252016-10-06 08:12:04 -0700539 assert buildbucket_host
540 assert changelist.GetIssue(), 'CL must be uploaded first'
541 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
542 patchset = patchset or changelist.GetMostRecentPatchset()
543 assert patchset, 'CL must be uploaded first'
544
545 codereview_url = changelist.GetCodereviewServer()
546 codereview_host = urlparse.urlparse(codereview_url).hostname
547 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000548 if authenticator.has_cached_credentials():
549 http = authenticator.authorize(httplib2.Http())
550 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700551 print('Warning: Some results might be missing because %s' %
552 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700553 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000554 http = httplib2.Http()
555
556 http.force_exception_to_status_code = True
557
tandrii221ab252016-10-06 08:12:04 -0700558 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
559 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
560 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000561 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700562 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000563 params = {'tag': 'buildset:%s' % buildset}
564
565 builds = {}
566 while True:
567 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700568 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000569 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700570 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000571 for build in content.get('builds', []):
572 builds[build['id']] = build
573 if 'next_cursor' in content:
574 params['start_cursor'] = content['next_cursor']
575 else:
576 break
577 return builds
578
579
qyearsleyeab3c042016-08-24 09:18:28 -0700580def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000581 """Prints nicely result of fetch_try_jobs."""
582 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700583 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000584 return
585
586 # Make a copy, because we'll be modifying builds dictionary.
587 builds = builds.copy()
588 builder_names_cache = {}
589
590 def get_builder(b):
591 try:
592 return builder_names_cache[b['id']]
593 except KeyError:
594 try:
595 parameters = json.loads(b['parameters_json'])
596 name = parameters['builder_name']
597 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700598 print('WARNING: failed to get builder name for build %s: %s' % (
599 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000600 name = None
601 builder_names_cache[b['id']] = name
602 return name
603
604 def get_bucket(b):
605 bucket = b['bucket']
606 if bucket.startswith('master.'):
607 return bucket[len('master.'):]
608 return bucket
609
610 if options.print_master:
611 name_fmt = '%%-%ds %%-%ds' % (
612 max(len(str(get_bucket(b))) for b in builds.itervalues()),
613 max(len(str(get_builder(b))) for b in builds.itervalues()))
614 def get_name(b):
615 return name_fmt % (get_bucket(b), get_builder(b))
616 else:
617 name_fmt = '%%-%ds' % (
618 max(len(str(get_builder(b))) for b in builds.itervalues()))
619 def get_name(b):
620 return name_fmt % get_builder(b)
621
622 def sort_key(b):
623 return b['status'], b.get('result'), get_name(b), b.get('url')
624
625 def pop(title, f, color=None, **kwargs):
626 """Pop matching builds from `builds` dict and print them."""
627
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000628 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000629 colorize = str
630 else:
631 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
632
633 result = []
634 for b in builds.values():
635 if all(b.get(k) == v for k, v in kwargs.iteritems()):
636 builds.pop(b['id'])
637 result.append(b)
638 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700639 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000640 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700641 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000642
643 total = len(builds)
644 pop(status='COMPLETED', result='SUCCESS',
645 title='Successes:', color=Fore.GREEN,
646 f=lambda b: (get_name(b), b.get('url')))
647 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
648 title='Infra Failures:', color=Fore.MAGENTA,
649 f=lambda b: (get_name(b), b.get('url')))
650 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
651 title='Failures:', color=Fore.RED,
652 f=lambda b: (get_name(b), b.get('url')))
653 pop(status='COMPLETED', result='CANCELED',
654 title='Canceled:', color=Fore.MAGENTA,
655 f=lambda b: (get_name(b),))
656 pop(status='COMPLETED', result='FAILURE',
657 failure_reason='INVALID_BUILD_DEFINITION',
658 title='Wrong master/builder name:', color=Fore.MAGENTA,
659 f=lambda b: (get_name(b),))
660 pop(status='COMPLETED', result='FAILURE',
661 title='Other failures:',
662 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
663 pop(status='COMPLETED',
664 title='Other finished:',
665 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
666 pop(status='STARTED',
667 title='Started:', color=Fore.YELLOW,
668 f=lambda b: (get_name(b), b.get('url')))
669 pop(status='SCHEDULED',
670 title='Scheduled:',
671 f=lambda b: (get_name(b), 'id=%s' % b['id']))
672 # The last section is just in case buildbucket API changes OR there is a bug.
673 pop(title='Other:',
674 f=lambda b: (get_name(b), 'id=%s' % b['id']))
675 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700676 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000677
678
qyearsley53f48a12016-09-01 10:45:13 -0700679def write_try_results_json(output_file, builds):
680 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
681
682 The input |builds| dict is assumed to be generated by Buildbucket.
683 Buildbucket documentation: http://goo.gl/G0s101
684 """
685
686 def convert_build_dict(build):
687 return {
688 'buildbucket_id': build.get('id'),
689 'status': build.get('status'),
690 'result': build.get('result'),
691 'bucket': build.get('bucket'),
692 'builder_name': json.loads(
693 build.get('parameters_json', '{}')).get('builder_name'),
694 'failure_reason': build.get('failure_reason'),
695 'url': build.get('url'),
696 }
697
698 converted = []
699 for _, build in sorted(builds.items()):
700 converted.append(convert_build_dict(build))
701 write_json(output_file, converted)
702
703
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000704def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
705 """Return the corresponding git ref if |base_url| together with |glob_spec|
706 matches the full |url|.
707
708 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
709 """
710 fetch_suburl, as_ref = glob_spec.split(':')
711 if allow_wildcards:
712 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
713 if glob_match:
714 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
715 # "branches/{472,597,648}/src:refs/remotes/svn/*".
716 branch_re = re.escape(base_url)
717 if glob_match.group(1):
718 branch_re += '/' + re.escape(glob_match.group(1))
719 wildcard = glob_match.group(2)
720 if wildcard == '*':
721 branch_re += '([^/]*)'
722 else:
723 # Escape and replace surrounding braces with parentheses and commas
724 # with pipe symbols.
725 wildcard = re.escape(wildcard)
726 wildcard = re.sub('^\\\\{', '(', wildcard)
727 wildcard = re.sub('\\\\,', '|', wildcard)
728 wildcard = re.sub('\\\\}$', ')', wildcard)
729 branch_re += wildcard
730 if glob_match.group(3):
731 branch_re += re.escape(glob_match.group(3))
732 match = re.match(branch_re, url)
733 if match:
734 return re.sub('\*$', match.group(1), as_ref)
735
736 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
737 if fetch_suburl:
738 full_url = base_url + '/' + fetch_suburl
739 else:
740 full_url = base_url
741 if full_url == url:
742 return as_ref
743 return None
744
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000745
iannucci@chromium.org79540052012-10-19 23:15:26 +0000746def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000747 """Prints statistics about the change to the user."""
748 # --no-ext-diff is broken in some versions of Git, so try to work around
749 # this by overriding the environment (but there is still a problem if the
750 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000751 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000752 if 'GIT_EXTERNAL_DIFF' in env:
753 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000754
755 if find_copies:
scottmgb84b5e32016-11-10 09:25:33 -0800756 similarity_options = ['-l100000', '-C%s' % similarity]
iannucci@chromium.org79540052012-10-19 23:15:26 +0000757 else:
758 similarity_options = ['-M%s' % similarity]
759
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000760 try:
761 stdout = sys.stdout.fileno()
762 except AttributeError:
763 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000764 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000765 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000766 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000767 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000768
769
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000770class BuildbucketResponseException(Exception):
771 pass
772
773
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000774class Settings(object):
775 def __init__(self):
776 self.default_server = None
777 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000778 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000779 self.is_git_svn = None
780 self.svn_branch = None
781 self.tree_status_url = None
782 self.viewvc_url = None
783 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000784 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000785 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000786 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000787 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000788 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000789 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000790 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000791
792 def LazyUpdateIfNeeded(self):
793 """Updates the settings from a codereview.settings file, if available."""
794 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000795 # The only value that actually changes the behavior is
796 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000797 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000798 error_ok=True
799 ).strip().lower()
800
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000801 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000802 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000803 LoadCodereviewSettingsFromFile(cr_settings_file)
804 self.updated = True
805
806 def GetDefaultServerUrl(self, error_ok=False):
807 if not self.default_server:
808 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000809 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000810 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000811 if error_ok:
812 return self.default_server
813 if not self.default_server:
814 error_message = ('Could not find settings file. You must configure '
815 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000816 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000817 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000818 return self.default_server
819
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000820 @staticmethod
821 def GetRelativeRoot():
822 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000823
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000824 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000825 if self.root is None:
826 self.root = os.path.abspath(self.GetRelativeRoot())
827 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000828
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000829 def GetGitMirror(self, remote='origin'):
830 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000831 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000832 if not os.path.isdir(local_url):
833 return None
834 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
835 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
836 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
837 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
838 if mirror.exists():
839 return mirror
840 return None
841
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000842 def GetIsGitSvn(self):
843 """Return true if this repo looks like it's using git-svn."""
844 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000845 if self.GetPendingRefPrefix():
846 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
847 self.is_git_svn = False
848 else:
849 # If you have any "svn-remote.*" config keys, we think you're using svn.
850 self.is_git_svn = RunGitWithCode(
851 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000852 return self.is_git_svn
853
854 def GetSVNBranch(self):
855 if self.svn_branch is None:
856 if not self.GetIsGitSvn():
857 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
858
859 # Try to figure out which remote branch we're based on.
860 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000861 # 1) iterate through our branch history and find the svn URL.
862 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000863
864 # regexp matching the git-svn line that contains the URL.
865 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
866
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000867 # We don't want to go through all of history, so read a line from the
868 # pipe at a time.
869 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000870 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000871 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
872 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000873 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000874 for line in proc.stdout:
875 match = git_svn_re.match(line)
876 if match:
877 url = match.group(1)
878 proc.stdout.close() # Cut pipe.
879 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000880
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000881 if url:
882 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
883 remotes = RunGit(['config', '--get-regexp',
884 r'^svn-remote\..*\.url']).splitlines()
885 for remote in remotes:
886 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000887 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000888 remote = match.group(1)
889 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000890 rewrite_root = RunGit(
891 ['config', 'svn-remote.%s.rewriteRoot' % remote],
892 error_ok=True).strip()
893 if rewrite_root:
894 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000895 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000896 ['config', 'svn-remote.%s.fetch' % remote],
897 error_ok=True).strip()
898 if fetch_spec:
899 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
900 if self.svn_branch:
901 break
902 branch_spec = RunGit(
903 ['config', 'svn-remote.%s.branches' % remote],
904 error_ok=True).strip()
905 if branch_spec:
906 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
907 if self.svn_branch:
908 break
909 tag_spec = RunGit(
910 ['config', 'svn-remote.%s.tags' % remote],
911 error_ok=True).strip()
912 if tag_spec:
913 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
914 if self.svn_branch:
915 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000916
917 if not self.svn_branch:
918 DieWithError('Can\'t guess svn branch -- try specifying it on the '
919 'command line')
920
921 return self.svn_branch
922
923 def GetTreeStatusUrl(self, error_ok=False):
924 if not self.tree_status_url:
925 error_message = ('You must configure your tree status URL by running '
926 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000927 self.tree_status_url = self._GetRietveldConfig(
928 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000929 return self.tree_status_url
930
931 def GetViewVCUrl(self):
932 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000933 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000934 return self.viewvc_url
935
rmistry@google.com90752582014-01-14 21:04:50 +0000936 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000937 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000938
rmistry@google.com78948ed2015-07-08 23:09:57 +0000939 def GetIsSkipDependencyUpload(self, branch_name):
940 """Returns true if specified branch should skip dep uploads."""
941 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
942 error_ok=True)
943
rmistry@google.com5626a922015-02-26 14:03:30 +0000944 def GetRunPostUploadHook(self):
945 run_post_upload_hook = self._GetRietveldConfig(
946 'run-post-upload-hook', error_ok=True)
947 return run_post_upload_hook == "True"
948
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000949 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000950 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000951
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000952 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000953 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000954
ukai@chromium.orge8077812012-02-03 03:41:46 +0000955 def GetIsGerrit(self):
956 """Return true if this repo is assosiated with gerrit code review system."""
957 if self.is_gerrit is None:
958 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
959 return self.is_gerrit
960
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000961 def GetSquashGerritUploads(self):
962 """Return true if uploads to Gerrit should be squashed by default."""
963 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700964 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
965 if self.squash_gerrit_uploads is None:
966 # Default is squash now (http://crbug.com/611892#c23).
967 self.squash_gerrit_uploads = not (
968 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
969 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000970 return self.squash_gerrit_uploads
971
tandriia60502f2016-06-20 02:01:53 -0700972 def GetSquashGerritUploadsOverride(self):
973 """Return True or False if codereview.settings should be overridden.
974
975 Returns None if no override has been defined.
976 """
977 # See also http://crbug.com/611892#c23
978 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
979 error_ok=True).strip()
980 if result == 'true':
981 return True
982 if result == 'false':
983 return False
984 return None
985
tandrii@chromium.org28253532016-04-14 13:46:56 +0000986 def GetGerritSkipEnsureAuthenticated(self):
987 """Return True if EnsureAuthenticated should not be done for Gerrit
988 uploads."""
989 if self.gerrit_skip_ensure_authenticated is None:
990 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000991 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000992 error_ok=True).strip() == 'true')
993 return self.gerrit_skip_ensure_authenticated
994
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000995 def GetGitEditor(self):
996 """Return the editor specified in the git config, or None if none is."""
997 if self.git_editor is None:
998 self.git_editor = self._GetConfig('core.editor', error_ok=True)
999 return self.git_editor or None
1000
thestig@chromium.org44202a22014-03-11 19:22:18 +00001001 def GetLintRegex(self):
1002 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
1003 DEFAULT_LINT_REGEX)
1004
1005 def GetLintIgnoreRegex(self):
1006 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
1007 DEFAULT_LINT_IGNORE_REGEX)
1008
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001009 def GetProject(self):
1010 if not self.project:
1011 self.project = self._GetRietveldConfig('project', error_ok=True)
1012 return self.project
1013
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001014 def GetForceHttpsCommitUrl(self):
1015 if not self.force_https_commit_url:
1016 self.force_https_commit_url = self._GetRietveldConfig(
1017 'force-https-commit-url', error_ok=True)
1018 return self.force_https_commit_url
1019
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001020 def GetPendingRefPrefix(self):
1021 if not self.pending_ref_prefix:
1022 self.pending_ref_prefix = self._GetRietveldConfig(
1023 'pending-ref-prefix', error_ok=True)
1024 return self.pending_ref_prefix
1025
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001026 def _GetRietveldConfig(self, param, **kwargs):
1027 return self._GetConfig('rietveld.' + param, **kwargs)
1028
rmistry@google.com78948ed2015-07-08 23:09:57 +00001029 def _GetBranchConfig(self, branch_name, param, **kwargs):
1030 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
1031
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001032 def _GetConfig(self, param, **kwargs):
1033 self.LazyUpdateIfNeeded()
1034 return RunGit(['config', param], **kwargs).strip()
1035
1036
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001037class _GitNumbererState(object):
1038 KNOWN_PROJECTS_WHITELIST = [
1039 'chromium/src',
1040 'external/webrtc',
1041 'v8/v8',
1042 ]
1043
1044 @classmethod
1045 def load(cls, remote_url, remote_ref):
1046 """Figures out the state by fetching special refs from remote repo.
1047 """
1048 assert remote_ref and remote_ref.startswith('refs/'), remote_ref
1049 url_parts = urlparse.urlparse(remote_url)
1050 project_name = url_parts.path.lstrip('/').rstrip('git./')
1051 for known in cls.KNOWN_PROJECTS_WHITELIST:
1052 if project_name.endswith(known):
1053 break
1054 else:
1055 # Early exit to avoid extra fetches for repos that aren't using gnumbd.
1056 return cls(cls._get_pending_prefix_fallback(), None)
1057
Quinten Yearsley442fb642016-12-15 15:38:27 -08001058 # This pollutes local ref space, but the amount of objects is negligible.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001059 error, _ = cls._run_git_with_code([
1060 'fetch', remote_url,
1061 '+refs/meta/config:refs/git_cl/meta/config',
1062 '+refs/gnumbd-config/main:refs/git_cl/gnumbd-config/main'])
1063 if error:
1064 # Some ref doesn't exist or isn't accessible to current user.
1065 # This shouldn't happen on production KNOWN_PROJECTS_WHITELIST
1066 # with git-numberer.
1067 cls._warn('failed to fetch gnumbd and project config for %s: %s',
1068 remote_url, error)
1069 return cls(cls._get_pending_prefix_fallback(), None)
1070 return cls(cls._get_pending_prefix(remote_ref),
1071 cls._is_validator_enabled(remote_ref))
1072
1073 @classmethod
1074 def _get_pending_prefix(cls, ref):
1075 error, gnumbd_config_data = cls._run_git_with_code(
1076 ['show', 'refs/git_cl/gnumbd-config/main:config.json'])
1077 if error:
1078 cls._warn('gnumbd config file not found')
1079 return cls._get_pending_prefix_fallback()
1080
1081 try:
1082 config = json.loads(gnumbd_config_data)
1083 if cls.match_refglobs(ref, config['enabled_refglobs']):
1084 return config['pending_ref_prefix']
1085 return None
1086 except KeyboardInterrupt:
1087 raise
1088 except Exception as e:
1089 cls._warn('failed to parse gnumbd config: %s', e)
1090 return cls._get_pending_prefix_fallback()
1091
1092 @staticmethod
1093 def _get_pending_prefix_fallback():
1094 global settings
1095 if not settings:
1096 settings = Settings()
1097 return settings.GetPendingRefPrefix()
1098
1099 @classmethod
1100 def _is_validator_enabled(cls, ref):
1101 error, project_config_data = cls._run_git_with_code(
1102 ['show', 'refs/git_cl/meta/config:project.config'])
1103 if error:
1104 cls._warn('project.config file not found')
1105 return False
1106 # Gerrit's project.config is really a git config file.
1107 # So, parse it as such.
1108 with tempfile.NamedTemporaryFile(prefix='git_cl_proj_config') as f:
1109 f.write(project_config_data)
1110 # Make sure OS sees this, but don't close the file just yet,
1111 # as NamedTemporaryFile deletes it on closing.
1112 f.flush()
1113
1114 def get_opts(x):
1115 code, out = cls._run_git_with_code(
1116 ['config', '-f', f.name, '--get-all',
1117 'plugin.git-numberer.validate-%s-refglob' % x])
1118 if code == 0:
1119 return out.strip().splitlines()
1120 return []
1121 enabled, disabled = map(get_opts, ['enabled', 'disabled'])
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01001122 logging.info('validator config enabled %s disabled %s refglobs for '
1123 '(this ref: %s)', enabled, disabled, ref)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001124
1125 if cls.match_refglobs(ref, disabled):
1126 return False
1127 return cls.match_refglobs(ref, enabled)
1128
1129 @staticmethod
1130 def match_refglobs(ref, refglobs):
1131 for refglob in refglobs:
1132 if ref == refglob or fnmatch.fnmatch(ref, refglob):
1133 return True
1134 return False
1135
1136 @staticmethod
1137 def _run_git_with_code(*args, **kwargs):
1138 # The only reason for this wrapper is easy porting of this code to CQ
1139 # codebase, which forked git_cl.py and checkouts.py long time ago.
1140 return RunGitWithCode(*args, **kwargs)
1141
1142 @staticmethod
1143 def _warn(msg, *args):
1144 if args:
1145 msg = msg % args
1146 print('WARNING: %s' % msg)
1147
1148 def __init__(self, pending_prefix, validator_enabled):
1149 # TODO(tandrii): remove pending_prefix after gnumbd is no more.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01001150 if pending_prefix:
1151 if not pending_prefix.endswith('/'):
1152 pending_prefix += '/'
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001153 self._pending_prefix = pending_prefix or None
1154 self._validator_enabled = validator_enabled or False
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01001155 logging.debug('_GitNumbererState(pending: %s, validator: %s)',
1156 self._pending_prefix, self._validator_enabled)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001157
1158 @property
1159 def pending_prefix(self):
1160 return self._pending_prefix
1161
1162 @property
Andrii Shyshkalov8f15f3e2016-12-14 15:43:49 +01001163 def should_add_git_number(self):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001164 return self._validator_enabled and self._pending_prefix is None
1165
1166
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001167def ShortBranchName(branch):
1168 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001169 return branch.replace('refs/heads/', '', 1)
1170
1171
1172def GetCurrentBranchRef():
1173 """Returns branch ref (e.g., refs/heads/master) or None."""
1174 return RunGit(['symbolic-ref', 'HEAD'],
1175 stderr=subprocess2.VOID, error_ok=True).strip() or None
1176
1177
1178def GetCurrentBranch():
1179 """Returns current branch or None.
1180
1181 For refs/heads/* branches, returns just last part. For others, full ref.
1182 """
1183 branchref = GetCurrentBranchRef()
1184 if branchref:
1185 return ShortBranchName(branchref)
1186 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001187
1188
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001189class _CQState(object):
1190 """Enum for states of CL with respect to Commit Queue."""
1191 NONE = 'none'
1192 DRY_RUN = 'dry_run'
1193 COMMIT = 'commit'
1194
1195 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1196
1197
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001198class _ParsedIssueNumberArgument(object):
1199 def __init__(self, issue=None, patchset=None, hostname=None):
1200 self.issue = issue
1201 self.patchset = patchset
1202 self.hostname = hostname
1203
1204 @property
1205 def valid(self):
1206 return self.issue is not None
1207
1208
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001209def ParseIssueNumberArgument(arg):
1210 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1211 fail_result = _ParsedIssueNumberArgument()
1212
1213 if arg.isdigit():
1214 return _ParsedIssueNumberArgument(issue=int(arg))
1215 if not arg.startswith('http'):
1216 return fail_result
1217 url = gclient_utils.UpgradeToHttps(arg)
1218 try:
1219 parsed_url = urlparse.urlparse(url)
1220 except ValueError:
1221 return fail_result
1222 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
1223 tmp = cls.ParseIssueURL(parsed_url)
1224 if tmp is not None:
1225 return tmp
1226 return fail_result
1227
1228
Aaron Gablea45ee112016-11-22 15:14:38 -08001229class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001230 def __init__(self, issue, url):
1231 self.issue = issue
1232 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001233 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001234
1235 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001236 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001237 self.issue, self.url)
1238
1239
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001240class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001241 """Changelist works with one changelist in local branch.
1242
1243 Supports two codereview backends: Rietveld or Gerrit, selected at object
1244 creation.
1245
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001246 Notes:
1247 * Not safe for concurrent multi-{thread,process} use.
1248 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001249 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001250 """
1251
1252 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1253 """Create a new ChangeList instance.
1254
1255 If issue is given, the codereview must be given too.
1256
1257 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1258 Otherwise, it's decided based on current configuration of the local branch,
1259 with default being 'rietveld' for backwards compatibility.
1260 See _load_codereview_impl for more details.
1261
1262 **kwargs will be passed directly to codereview implementation.
1263 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001264 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001265 global settings
1266 if not settings:
1267 # Happens when git_cl.py is used as a utility library.
1268 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001269
1270 if issue:
1271 assert codereview, 'codereview must be known, if issue is known'
1272
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001273 self.branchref = branchref
1274 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001275 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001276 self.branch = ShortBranchName(self.branchref)
1277 else:
1278 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001279 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001280 self.lookedup_issue = False
1281 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001282 self.has_description = False
1283 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001284 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001285 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001286 self.cc = None
1287 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001288 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001289
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001290 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001291 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001292 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001293 assert self._codereview_impl
1294 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001295
1296 def _load_codereview_impl(self, codereview=None, **kwargs):
1297 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001298 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1299 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1300 self._codereview = codereview
1301 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001302 return
1303
1304 # Automatic selection based on issue number set for a current branch.
1305 # Rietveld takes precedence over Gerrit.
1306 assert not self.issue
1307 # Whether we find issue or not, we are doing the lookup.
1308 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001309 if self.GetBranch():
1310 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1311 issue = _git_get_branch_config_value(
1312 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1313 if issue:
1314 self._codereview = codereview
1315 self._codereview_impl = cls(self, **kwargs)
1316 self.issue = int(issue)
1317 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001318
1319 # No issue is set for this branch, so decide based on repo-wide settings.
1320 return self._load_codereview_impl(
1321 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1322 **kwargs)
1323
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001324 def IsGerrit(self):
1325 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001326
1327 def GetCCList(self):
1328 """Return the users cc'd on this CL.
1329
agable92bec4f2016-08-24 09:27:27 -07001330 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001331 """
1332 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001333 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001334 more_cc = ','.join(self.watchers)
1335 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1336 return self.cc
1337
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001338 def GetCCListWithoutDefault(self):
1339 """Return the users cc'd on this CL excluding default ones."""
1340 if self.cc is None:
1341 self.cc = ','.join(self.watchers)
1342 return self.cc
1343
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001344 def SetWatchers(self, watchers):
1345 """Set the list of email addresses that should be cc'd based on the changed
1346 files in this CL.
1347 """
1348 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001349
1350 def GetBranch(self):
1351 """Returns the short branch name, e.g. 'master'."""
1352 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001353 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001354 if not branchref:
1355 return None
1356 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001357 self.branch = ShortBranchName(self.branchref)
1358 return self.branch
1359
1360 def GetBranchRef(self):
1361 """Returns the full branch name, e.g. 'refs/heads/master'."""
1362 self.GetBranch() # Poke the lazy loader.
1363 return self.branchref
1364
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001365 def ClearBranch(self):
1366 """Clears cached branch data of this object."""
1367 self.branch = self.branchref = None
1368
tandrii5d48c322016-08-18 16:19:37 -07001369 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1370 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1371 kwargs['branch'] = self.GetBranch()
1372 return _git_get_branch_config_value(key, default, **kwargs)
1373
1374 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1375 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1376 assert self.GetBranch(), (
1377 'this CL must have an associated branch to %sset %s%s' %
1378 ('un' if value is None else '',
1379 key,
1380 '' if value is None else ' to %r' % value))
1381 kwargs['branch'] = self.GetBranch()
1382 return _git_set_branch_config_value(key, value, **kwargs)
1383
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001384 @staticmethod
1385 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001386 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001387 e.g. 'origin', 'refs/heads/master'
1388 """
1389 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001390 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1391
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001392 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001393 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001394 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001395 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1396 error_ok=True).strip()
1397 if upstream_branch:
1398 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001399 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001400 # Fall back on trying a git-svn upstream branch.
1401 if settings.GetIsGitSvn():
1402 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001403 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001404 # Else, try to guess the origin remote.
1405 remote_branches = RunGit(['branch', '-r']).split()
1406 if 'origin/master' in remote_branches:
1407 # Fall back on origin/master if it exits.
1408 remote = 'origin'
1409 upstream_branch = 'refs/heads/master'
1410 elif 'origin/trunk' in remote_branches:
1411 # Fall back on origin/trunk if it exists. Generally a shared
1412 # git-svn clone
1413 remote = 'origin'
1414 upstream_branch = 'refs/heads/trunk'
1415 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001416 DieWithError(
1417 'Unable to determine default branch to diff against.\n'
1418 'Either pass complete "git diff"-style arguments, like\n'
1419 ' git cl upload origin/master\n'
1420 'or verify this branch is set up to track another \n'
1421 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001422
1423 return remote, upstream_branch
1424
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001425 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001426 upstream_branch = self.GetUpstreamBranch()
1427 if not BranchExists(upstream_branch):
1428 DieWithError('The upstream for the current branch (%s) does not exist '
1429 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001430 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001431 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001432
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001433 def GetUpstreamBranch(self):
1434 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001435 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001436 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001437 upstream_branch = upstream_branch.replace('refs/heads/',
1438 'refs/remotes/%s/' % remote)
1439 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1440 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001441 self.upstream_branch = upstream_branch
1442 return self.upstream_branch
1443
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001444 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001445 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001446 remote, branch = None, self.GetBranch()
1447 seen_branches = set()
1448 while branch not in seen_branches:
1449 seen_branches.add(branch)
1450 remote, branch = self.FetchUpstreamTuple(branch)
1451 branch = ShortBranchName(branch)
1452 if remote != '.' or branch.startswith('refs/remotes'):
1453 break
1454 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001455 remotes = RunGit(['remote'], error_ok=True).split()
1456 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001457 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001458 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001459 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001460 logging.warning('Could not determine which remote this change is '
1461 'associated with, so defaulting to "%s". This may '
1462 'not be what you want. You may prevent this message '
1463 'by running "git svn info" as documented here: %s',
1464 self._remote,
1465 GIT_INSTRUCTIONS_URL)
1466 else:
1467 logging.warn('Could not determine which remote this change is '
1468 'associated with. You may prevent this message by '
1469 'running "git svn info" as documented here: %s',
1470 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001471 branch = 'HEAD'
1472 if branch.startswith('refs/remotes'):
1473 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001474 elif branch.startswith('refs/branch-heads/'):
1475 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001476 else:
1477 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001478 return self._remote
1479
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001480 def GitSanityChecks(self, upstream_git_obj):
1481 """Checks git repo status and ensures diff is from local commits."""
1482
sbc@chromium.org79706062015-01-14 21:18:12 +00001483 if upstream_git_obj is None:
1484 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001485 print('ERROR: unable to determine current branch (detached HEAD?)',
1486 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001487 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001488 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001489 return False
1490
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001491 # Verify the commit we're diffing against is in our current branch.
1492 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1493 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1494 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001495 print('ERROR: %s is not in the current branch. You may need to rebase '
1496 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001497 return False
1498
1499 # List the commits inside the diff, and verify they are all local.
1500 commits_in_diff = RunGit(
1501 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1502 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1503 remote_branch = remote_branch.strip()
1504 if code != 0:
1505 _, remote_branch = self.GetRemoteBranch()
1506
1507 commits_in_remote = RunGit(
1508 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1509
1510 common_commits = set(commits_in_diff) & set(commits_in_remote)
1511 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001512 print('ERROR: Your diff contains %d commits already in %s.\n'
1513 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1514 'the diff. If you are using a custom git flow, you can override'
1515 ' the reference used for this check with "git config '
1516 'gitcl.remotebranch <git-ref>".' % (
1517 len(common_commits), remote_branch, upstream_git_obj),
1518 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001519 return False
1520 return True
1521
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001522 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001523 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001524
1525 Returns None if it is not set.
1526 """
tandrii5d48c322016-08-18 16:19:37 -07001527 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001528
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001529 def GetGitSvnRemoteUrl(self):
1530 """Return the configured git-svn remote URL parsed from git svn info.
1531
1532 Returns None if it is not set.
1533 """
1534 # URL is dependent on the current directory.
1535 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1536 if data:
1537 keys = dict(line.split(': ', 1) for line in data.splitlines()
1538 if ': ' in line)
1539 return keys.get('URL', None)
1540 return None
1541
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001542 def GetRemoteUrl(self):
1543 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1544
1545 Returns None if there is no remote.
1546 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001547 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001548 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1549
1550 # If URL is pointing to a local directory, it is probably a git cache.
1551 if os.path.isdir(url):
1552 url = RunGit(['config', 'remote.%s.url' % remote],
1553 error_ok=True,
1554 cwd=url).strip()
1555 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001556
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001557 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001558 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001559 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001560 self.issue = self._GitGetBranchConfigValue(
1561 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001562 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001563 return self.issue
1564
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001565 def GetIssueURL(self):
1566 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001567 issue = self.GetIssue()
1568 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001569 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001570 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001571
1572 def GetDescription(self, pretty=False):
1573 if not self.has_description:
1574 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001575 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001576 self.has_description = True
1577 if pretty:
1578 wrapper = textwrap.TextWrapper()
1579 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1580 return wrapper.fill(self.description)
1581 return self.description
1582
1583 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001584 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001585 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001586 self.patchset = self._GitGetBranchConfigValue(
1587 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001588 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001589 return self.patchset
1590
1591 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001592 """Set this branch's patchset. If patchset=0, clears the patchset."""
1593 assert self.GetBranch()
1594 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001595 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001596 else:
1597 self.patchset = int(patchset)
1598 self._GitSetBranchConfigValue(
1599 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001600
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001601 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001602 """Set this branch's issue. If issue isn't given, clears the issue."""
1603 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001604 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001605 issue = int(issue)
1606 self._GitSetBranchConfigValue(
1607 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001608 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001609 codereview_server = self._codereview_impl.GetCodereviewServer()
1610 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001611 self._GitSetBranchConfigValue(
1612 self._codereview_impl.CodereviewServerConfigKey(),
1613 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001614 else:
tandrii5d48c322016-08-18 16:19:37 -07001615 # Reset all of these just to be clean.
1616 reset_suffixes = [
1617 'last-upload-hash',
1618 self._codereview_impl.IssueConfigKey(),
1619 self._codereview_impl.PatchsetConfigKey(),
1620 self._codereview_impl.CodereviewServerConfigKey(),
1621 ] + self._PostUnsetIssueProperties()
1622 for prop in reset_suffixes:
1623 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001624 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001625 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001626
dnjba1b0f32016-09-02 12:37:42 -07001627 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001628 if not self.GitSanityChecks(upstream_branch):
1629 DieWithError('\nGit sanity check failure')
1630
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001631 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001632 if not root:
1633 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001634 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001635
1636 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001637 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001638 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001639 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001640 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001641 except subprocess2.CalledProcessError:
1642 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001643 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001644 'This branch probably doesn\'t exist anymore. To reset the\n'
1645 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001646 ' git branch --set-upstream-to origin/master %s\n'
1647 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001648 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001649
maruel@chromium.org52424302012-08-29 15:14:30 +00001650 issue = self.GetIssue()
1651 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001652 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001653 description = self.GetDescription()
1654 else:
1655 # If the change was never uploaded, use the log messages of all commits
1656 # up to the branch point, as git cl upload will prefill the description
1657 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001658 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1659 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001660
1661 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001662 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001663 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001664 name,
1665 description,
1666 absroot,
1667 files,
1668 issue,
1669 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001670 author,
1671 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001672
dsansomee2d6fd92016-09-08 00:10:47 -07001673 def UpdateDescription(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001674 self.description = description
dsansomee2d6fd92016-09-08 00:10:47 -07001675 return self._codereview_impl.UpdateDescriptionRemote(
1676 description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001677
1678 def RunHook(self, committing, may_prompt, verbose, change):
1679 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1680 try:
1681 return presubmit_support.DoPresubmitChecks(change, committing,
1682 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1683 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001684 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1685 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001686 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001687 DieWithError(
1688 ('%s\nMaybe your depot_tools is out of date?\n'
1689 'If all fails, contact maruel@') % e)
1690
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001691 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1692 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001693 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1694 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001695 else:
1696 # Assume url.
1697 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1698 urlparse.urlparse(issue_arg))
1699 if not parsed_issue_arg or not parsed_issue_arg.valid:
1700 DieWithError('Failed to parse issue argument "%s". '
1701 'Must be an issue number or a valid URL.' % issue_arg)
1702 return self._codereview_impl.CMDPatchWithParsedIssue(
1703 parsed_issue_arg, reject, nocommit, directory)
1704
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001705 def CMDUpload(self, options, git_diff_args, orig_args):
1706 """Uploads a change to codereview."""
1707 if git_diff_args:
1708 # TODO(ukai): is it ok for gerrit case?
1709 base_branch = git_diff_args[0]
1710 else:
1711 if self.GetBranch() is None:
1712 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1713
1714 # Default to diffing against common ancestor of upstream branch
1715 base_branch = self.GetCommonAncestorWithUpstream()
1716 git_diff_args = [base_branch, 'HEAD']
1717
1718 # Make sure authenticated to codereview before running potentially expensive
1719 # hooks. It is a fast, best efforts check. Codereview still can reject the
1720 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001721 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001722
1723 # Apply watchlists on upload.
1724 change = self.GetChange(base_branch, None)
1725 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1726 files = [f.LocalPath() for f in change.AffectedFiles()]
1727 if not options.bypass_watchlists:
1728 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1729
1730 if not options.bypass_hooks:
1731 if options.reviewers or options.tbr_owners:
1732 # Set the reviewer list now so that presubmit checks can access it.
1733 change_description = ChangeDescription(change.FullDescriptionText())
1734 change_description.update_reviewers(options.reviewers,
1735 options.tbr_owners,
1736 change)
1737 change.SetDescriptionText(change_description.description)
1738 hook_results = self.RunHook(committing=False,
1739 may_prompt=not options.force,
1740 verbose=options.verbose,
1741 change=change)
1742 if not hook_results.should_continue():
1743 return 1
1744 if not options.reviewers and hook_results.reviewers:
1745 options.reviewers = hook_results.reviewers.split(',')
1746
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001747 # TODO(tandrii): Checking local patchset against remote patchset is only
1748 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1749 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001750 latest_patchset = self.GetMostRecentPatchset()
1751 local_patchset = self.GetPatchset()
1752 if (latest_patchset and local_patchset and
1753 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001754 print('The last upload made from this repository was patchset #%d but '
1755 'the most recent patchset on the server is #%d.'
1756 % (local_patchset, latest_patchset))
1757 print('Uploading will still work, but if you\'ve uploaded to this '
1758 'issue from another machine or branch the patch you\'re '
1759 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001760 ask_for_data('About to upload; enter to confirm.')
1761
1762 print_stats(options.similarity, options.find_copies, git_diff_args)
1763 ret = self.CMDUploadChange(options, git_diff_args, change)
1764 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001765 if options.use_commit_queue:
1766 self.SetCQState(_CQState.COMMIT)
1767 elif options.cq_dry_run:
1768 self.SetCQState(_CQState.DRY_RUN)
1769
tandrii5d48c322016-08-18 16:19:37 -07001770 _git_set_branch_config_value('last-upload-hash',
1771 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001772 # Run post upload hooks, if specified.
1773 if settings.GetRunPostUploadHook():
1774 presubmit_support.DoPostUploadExecuter(
1775 change,
1776 self,
1777 settings.GetRoot(),
1778 options.verbose,
1779 sys.stdout)
1780
1781 # Upload all dependencies if specified.
1782 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001783 print()
1784 print('--dependencies has been specified.')
1785 print('All dependent local branches will be re-uploaded.')
1786 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001787 # Remove the dependencies flag from args so that we do not end up in a
1788 # loop.
1789 orig_args.remove('--dependencies')
1790 ret = upload_branch_deps(self, orig_args)
1791 return ret
1792
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001793 def SetCQState(self, new_state):
1794 """Update the CQ state for latest patchset.
1795
1796 Issue must have been already uploaded and known.
1797 """
1798 assert new_state in _CQState.ALL_STATES
1799 assert self.GetIssue()
1800 return self._codereview_impl.SetCQState(new_state)
1801
qyearsley1fdfcb62016-10-24 13:22:03 -07001802 def TriggerDryRun(self):
1803 """Triggers a dry run and prints a warning on failure."""
1804 # TODO(qyearsley): Either re-use this method in CMDset_commit
1805 # and CMDupload, or change CMDtry to trigger dry runs with
1806 # just SetCQState, and catch keyboard interrupt and other
1807 # errors in that method.
1808 try:
1809 self.SetCQState(_CQState.DRY_RUN)
1810 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1811 return 0
1812 except KeyboardInterrupt:
1813 raise
1814 except:
1815 print('WARNING: failed to trigger CQ Dry Run.\n'
1816 'Either:\n'
1817 ' * your project has no CQ\n'
1818 ' * you don\'t have permission to trigger Dry Run\n'
1819 ' * bug in this code (see stack trace below).\n'
1820 'Consider specifying which bots to trigger manually '
1821 'or asking your project owners for permissions '
1822 'or contacting Chrome Infrastructure team at '
1823 'https://www.chromium.org/infra\n\n')
1824 # Still raise exception so that stack trace is printed.
1825 raise
1826
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001827 # Forward methods to codereview specific implementation.
1828
1829 def CloseIssue(self):
1830 return self._codereview_impl.CloseIssue()
1831
1832 def GetStatus(self):
1833 return self._codereview_impl.GetStatus()
1834
1835 def GetCodereviewServer(self):
1836 return self._codereview_impl.GetCodereviewServer()
1837
tandriide281ae2016-10-12 06:02:30 -07001838 def GetIssueOwner(self):
1839 """Get owner from codereview, which may differ from this checkout."""
1840 return self._codereview_impl.GetIssueOwner()
1841
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001842 def GetApprovingReviewers(self):
1843 return self._codereview_impl.GetApprovingReviewers()
1844
1845 def GetMostRecentPatchset(self):
1846 return self._codereview_impl.GetMostRecentPatchset()
1847
tandriide281ae2016-10-12 06:02:30 -07001848 def CannotTriggerTryJobReason(self):
1849 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1850 return self._codereview_impl.CannotTriggerTryJobReason()
1851
tandrii8c5a3532016-11-04 07:52:02 -07001852 def GetTryjobProperties(self, patchset=None):
1853 """Returns dictionary of properties to launch tryjob."""
1854 return self._codereview_impl.GetTryjobProperties(patchset=patchset)
1855
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001856 def __getattr__(self, attr):
1857 # This is because lots of untested code accesses Rietveld-specific stuff
1858 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001859 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001860 # Note that child method defines __getattr__ as well, and forwards it here,
1861 # because _RietveldChangelistImpl is not cleaned up yet, and given
1862 # deprecation of Rietveld, it should probably be just removed.
1863 # Until that time, avoid infinite recursion by bypassing __getattr__
1864 # of implementation class.
1865 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001866
1867
1868class _ChangelistCodereviewBase(object):
1869 """Abstract base class encapsulating codereview specifics of a changelist."""
1870 def __init__(self, changelist):
1871 self._changelist = changelist # instance of Changelist
1872
1873 def __getattr__(self, attr):
1874 # Forward methods to changelist.
1875 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1876 # _RietveldChangelistImpl to avoid this hack?
1877 return getattr(self._changelist, attr)
1878
1879 def GetStatus(self):
1880 """Apply a rough heuristic to give a simple summary of an issue's review
1881 or CQ status, assuming adherence to a common workflow.
1882
1883 Returns None if no issue for this branch, or specific string keywords.
1884 """
1885 raise NotImplementedError()
1886
1887 def GetCodereviewServer(self):
1888 """Returns server URL without end slash, like "https://codereview.com"."""
1889 raise NotImplementedError()
1890
1891 def FetchDescription(self):
1892 """Fetches and returns description from the codereview server."""
1893 raise NotImplementedError()
1894
tandrii5d48c322016-08-18 16:19:37 -07001895 @classmethod
1896 def IssueConfigKey(cls):
1897 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001898 raise NotImplementedError()
1899
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001900 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001901 def PatchsetConfigKey(cls):
1902 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001903 raise NotImplementedError()
1904
tandrii5d48c322016-08-18 16:19:37 -07001905 @classmethod
1906 def CodereviewServerConfigKey(cls):
1907 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001908 raise NotImplementedError()
1909
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001910 def _PostUnsetIssueProperties(self):
1911 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001912 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001913
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001914 def GetRieveldObjForPresubmit(self):
1915 # This is an unfortunate Rietveld-embeddedness in presubmit.
1916 # For non-Rietveld codereviews, this probably should return a dummy object.
1917 raise NotImplementedError()
1918
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001919 def GetGerritObjForPresubmit(self):
1920 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1921 return None
1922
dsansomee2d6fd92016-09-08 00:10:47 -07001923 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001924 """Update the description on codereview site."""
1925 raise NotImplementedError()
1926
1927 def CloseIssue(self):
1928 """Closes the issue."""
1929 raise NotImplementedError()
1930
1931 def GetApprovingReviewers(self):
1932 """Returns a list of reviewers approving the change.
1933
1934 Note: not necessarily committers.
1935 """
1936 raise NotImplementedError()
1937
1938 def GetMostRecentPatchset(self):
1939 """Returns the most recent patchset number from the codereview site."""
1940 raise NotImplementedError()
1941
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001942 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1943 directory):
1944 """Fetches and applies the issue.
1945
1946 Arguments:
1947 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1948 reject: if True, reject the failed patch instead of switching to 3-way
1949 merge. Rietveld only.
1950 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1951 only.
1952 directory: switch to directory before applying the patch. Rietveld only.
1953 """
1954 raise NotImplementedError()
1955
1956 @staticmethod
1957 def ParseIssueURL(parsed_url):
1958 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1959 failed."""
1960 raise NotImplementedError()
1961
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001962 def EnsureAuthenticated(self, force):
1963 """Best effort check that user is authenticated with codereview server.
1964
1965 Arguments:
1966 force: whether to skip confirmation questions.
1967 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001968 raise NotImplementedError()
1969
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001970 def CMDUploadChange(self, options, args, change):
1971 """Uploads a change to codereview."""
1972 raise NotImplementedError()
1973
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001974 def SetCQState(self, new_state):
1975 """Update the CQ state for latest patchset.
1976
1977 Issue must have been already uploaded and known.
1978 """
1979 raise NotImplementedError()
1980
tandriie113dfd2016-10-11 10:20:12 -07001981 def CannotTriggerTryJobReason(self):
1982 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1983 raise NotImplementedError()
1984
tandriide281ae2016-10-12 06:02:30 -07001985 def GetIssueOwner(self):
1986 raise NotImplementedError()
1987
tandrii8c5a3532016-11-04 07:52:02 -07001988 def GetTryjobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001989 raise NotImplementedError()
1990
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001991
1992class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1993 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1994 super(_RietveldChangelistImpl, self).__init__(changelist)
1995 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001996 if not rietveld_server:
1997 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001998
1999 self._rietveld_server = rietveld_server
2000 self._auth_config = auth_config
2001 self._props = None
2002 self._rpc_server = None
2003
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002004 def GetCodereviewServer(self):
2005 if not self._rietveld_server:
2006 # If we're on a branch then get the server potentially associated
2007 # with that branch.
2008 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002009 self._rietveld_server = gclient_utils.UpgradeToHttps(
2010 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002011 if not self._rietveld_server:
2012 self._rietveld_server = settings.GetDefaultServerUrl()
2013 return self._rietveld_server
2014
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002015 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002016 """Best effort check that user is authenticated with Rietveld server."""
2017 if self._auth_config.use_oauth2:
2018 authenticator = auth.get_authenticator_for_host(
2019 self.GetCodereviewServer(), self._auth_config)
2020 if not authenticator.has_cached_credentials():
2021 raise auth.LoginRequiredError(self.GetCodereviewServer())
2022
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002023 def FetchDescription(self):
2024 issue = self.GetIssue()
2025 assert issue
2026 try:
2027 return self.RpcServer().get_description(issue).strip()
2028 except urllib2.HTTPError as e:
2029 if e.code == 404:
2030 DieWithError(
2031 ('\nWhile fetching the description for issue %d, received a '
2032 '404 (not found)\n'
2033 'error. It is likely that you deleted this '
2034 'issue on the server. If this is the\n'
2035 'case, please run\n\n'
2036 ' git cl issue 0\n\n'
2037 'to clear the association with the deleted issue. Then run '
2038 'this command again.') % issue)
2039 else:
2040 DieWithError(
2041 '\nFailed to fetch issue description. HTTP error %d' % e.code)
2042 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07002043 print('Warning: Failed to retrieve CL description due to network '
2044 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002045 return ''
2046
2047 def GetMostRecentPatchset(self):
2048 return self.GetIssueProperties()['patchsets'][-1]
2049
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002050 def GetIssueProperties(self):
2051 if self._props is None:
2052 issue = self.GetIssue()
2053 if not issue:
2054 self._props = {}
2055 else:
2056 self._props = self.RpcServer().get_issue_properties(issue, True)
2057 return self._props
2058
tandriie113dfd2016-10-11 10:20:12 -07002059 def CannotTriggerTryJobReason(self):
2060 props = self.GetIssueProperties()
2061 if not props:
2062 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
2063 if props.get('closed'):
2064 return 'CL %s is closed' % self.GetIssue()
2065 if props.get('private'):
2066 return 'CL %s is private' % self.GetIssue()
2067 return None
2068
tandrii8c5a3532016-11-04 07:52:02 -07002069 def GetTryjobProperties(self, patchset=None):
2070 """Returns dictionary of properties to launch tryjob."""
2071 project = (self.GetIssueProperties() or {}).get('project')
2072 return {
2073 'issue': self.GetIssue(),
2074 'patch_project': project,
2075 'patch_storage': 'rietveld',
2076 'patchset': patchset or self.GetPatchset(),
2077 'rietveld': self.GetCodereviewServer(),
2078 }
2079
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002080 def GetApprovingReviewers(self):
2081 return get_approving_reviewers(self.GetIssueProperties())
2082
tandriide281ae2016-10-12 06:02:30 -07002083 def GetIssueOwner(self):
2084 return (self.GetIssueProperties() or {}).get('owner_email')
2085
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002086 def AddComment(self, message):
2087 return self.RpcServer().add_comment(self.GetIssue(), message)
2088
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002089 def GetStatus(self):
2090 """Apply a rough heuristic to give a simple summary of an issue's review
2091 or CQ status, assuming adherence to a common workflow.
2092
2093 Returns None if no issue for this branch, or one of the following keywords:
2094 * 'error' - error from review tool (including deleted issues)
2095 * 'unsent' - not sent for review
2096 * 'waiting' - waiting for review
2097 * 'reply' - waiting for owner to reply to review
2098 * 'lgtm' - LGTM from at least one approved reviewer
2099 * 'commit' - in the commit queue
2100 * 'closed' - closed
2101 """
2102 if not self.GetIssue():
2103 return None
2104
2105 try:
2106 props = self.GetIssueProperties()
2107 except urllib2.HTTPError:
2108 return 'error'
2109
2110 if props.get('closed'):
2111 # Issue is closed.
2112 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00002113 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002114 # Issue is in the commit queue.
2115 return 'commit'
2116
2117 try:
2118 reviewers = self.GetApprovingReviewers()
2119 except urllib2.HTTPError:
2120 return 'error'
2121
2122 if reviewers:
2123 # Was LGTM'ed.
2124 return 'lgtm'
2125
2126 messages = props.get('messages') or []
2127
tandrii9d2c7a32016-06-22 03:42:45 -07002128 # Skip CQ messages that don't require owner's action.
2129 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
2130 if 'Dry run:' in messages[-1]['text']:
2131 messages.pop()
2132 elif 'The CQ bit was unchecked' in messages[-1]['text']:
2133 # This message always follows prior messages from CQ,
2134 # so skip this too.
2135 messages.pop()
2136 else:
2137 # This is probably a CQ messages warranting user attention.
2138 break
2139
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002140 if not messages:
2141 # No message was sent.
2142 return 'unsent'
2143 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07002144 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002145 return 'reply'
2146 return 'waiting'
2147
dsansomee2d6fd92016-09-08 00:10:47 -07002148 def UpdateDescriptionRemote(self, description, force=False):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002149 return self.RpcServer().update_description(
2150 self.GetIssue(), self.description)
2151
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002152 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002153 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002154
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002155 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002156 return self.SetFlags({flag: value})
2157
2158 def SetFlags(self, flags):
2159 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002160 """
phajdan.jr68598232016-08-10 03:28:28 -07002161 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002162 try:
tandrii4b233bd2016-07-06 03:50:29 -07002163 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002164 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002165 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002166 if e.code == 404:
2167 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2168 if e.code == 403:
2169 DieWithError(
2170 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002171 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002172 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002173
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002174 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002175 """Returns an upload.RpcServer() to access this review's rietveld instance.
2176 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002177 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002178 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002179 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002180 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002181 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002182
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002183 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002184 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002185 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002186
tandrii5d48c322016-08-18 16:19:37 -07002187 @classmethod
2188 def PatchsetConfigKey(cls):
2189 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002190
tandrii5d48c322016-08-18 16:19:37 -07002191 @classmethod
2192 def CodereviewServerConfigKey(cls):
2193 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002194
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002195 def GetRieveldObjForPresubmit(self):
2196 return self.RpcServer()
2197
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002198 def SetCQState(self, new_state):
2199 props = self.GetIssueProperties()
2200 if props.get('private'):
2201 DieWithError('Cannot set-commit on private issue')
2202
2203 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002204 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002205 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002206 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002207 else:
tandrii4b233bd2016-07-06 03:50:29 -07002208 assert new_state == _CQState.DRY_RUN
2209 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002210
2211
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002212 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2213 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002214 # PatchIssue should never be called with a dirty tree. It is up to the
2215 # caller to check this, but just in case we assert here since the
2216 # consequences of the caller not checking this could be dire.
2217 assert(not git_common.is_dirty_git_tree('apply'))
2218 assert(parsed_issue_arg.valid)
2219 self._changelist.issue = parsed_issue_arg.issue
2220 if parsed_issue_arg.hostname:
2221 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2222
skobes6468b902016-10-24 08:45:10 -07002223 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2224 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2225 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002226 try:
skobes6468b902016-10-24 08:45:10 -07002227 scm_obj.apply_patch(patchset_object)
2228 except Exception as e:
2229 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002230 return 1
2231
2232 # If we had an issue, commit the current state and register the issue.
2233 if not nocommit:
2234 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2235 'patch from issue %(i)s at patchset '
2236 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2237 % {'i': self.GetIssue(), 'p': patchset})])
2238 self.SetIssue(self.GetIssue())
2239 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07002240 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002241 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002242 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002243 return 0
2244
2245 @staticmethod
2246 def ParseIssueURL(parsed_url):
2247 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2248 return None
wychen3c1c1722016-08-04 11:46:36 -07002249 # Rietveld patch: https://domain/<number>/#ps<patchset>
2250 match = re.match(r'/(\d+)/$', parsed_url.path)
2251 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2252 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002253 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002254 issue=int(match.group(1)),
2255 patchset=int(match2.group(1)),
2256 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002257 # Typical url: https://domain/<issue_number>[/[other]]
2258 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2259 if match:
skobes6468b902016-10-24 08:45:10 -07002260 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002261 issue=int(match.group(1)),
2262 hostname=parsed_url.netloc)
2263 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2264 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2265 if match:
skobes6468b902016-10-24 08:45:10 -07002266 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002267 issue=int(match.group(1)),
2268 patchset=int(match.group(2)),
skobes6468b902016-10-24 08:45:10 -07002269 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002270 return None
2271
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002272 def CMDUploadChange(self, options, args, change):
2273 """Upload the patch to Rietveld."""
2274 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2275 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002276 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2277 if options.emulate_svn_auto_props:
2278 upload_args.append('--emulate_svn_auto_props')
2279
2280 change_desc = None
2281
2282 if options.email is not None:
2283 upload_args.extend(['--email', options.email])
2284
2285 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002286 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002287 upload_args.extend(['--title', options.title])
2288 if options.message:
2289 upload_args.extend(['--message', options.message])
2290 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002291 print('This branch is associated with issue %s. '
2292 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002293 else:
nodirca166002016-06-27 10:59:51 -07002294 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002295 upload_args.extend(['--title', options.title])
2296 message = (options.title or options.message or
2297 CreateDescriptionFromLog(args))
2298 change_desc = ChangeDescription(message)
2299 if options.reviewers or options.tbr_owners:
2300 change_desc.update_reviewers(options.reviewers,
2301 options.tbr_owners,
2302 change)
2303 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002304 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002305
2306 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002307 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002308 return 1
2309
2310 upload_args.extend(['--message', change_desc.description])
2311 if change_desc.get_reviewers():
2312 upload_args.append('--reviewers=%s' % ','.join(
2313 change_desc.get_reviewers()))
2314 if options.send_mail:
2315 if not change_desc.get_reviewers():
2316 DieWithError("Must specify reviewers to send email.")
2317 upload_args.append('--send_mail')
2318
2319 # We check this before applying rietveld.private assuming that in
2320 # rietveld.cc only addresses which we can send private CLs to are listed
2321 # if rietveld.private is set, and so we should ignore rietveld.cc only
2322 # when --private is specified explicitly on the command line.
2323 if options.private:
2324 logging.warn('rietveld.cc is ignored since private flag is specified. '
2325 'You need to review and add them manually if necessary.')
2326 cc = self.GetCCListWithoutDefault()
2327 else:
2328 cc = self.GetCCList()
2329 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002330 if change_desc.get_cced():
2331 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002332 if cc:
2333 upload_args.extend(['--cc', cc])
2334
2335 if options.private or settings.GetDefaultPrivateFlag() == "True":
2336 upload_args.append('--private')
2337
2338 upload_args.extend(['--git_similarity', str(options.similarity)])
2339 if not options.find_copies:
2340 upload_args.extend(['--git_no_find_copies'])
2341
2342 # Include the upstream repo's URL in the change -- this is useful for
2343 # projects that have their source spread across multiple repos.
2344 remote_url = self.GetGitBaseUrlFromConfig()
2345 if not remote_url:
2346 if settings.GetIsGitSvn():
2347 remote_url = self.GetGitSvnRemoteUrl()
2348 else:
2349 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2350 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2351 self.GetUpstreamBranch().split('/')[-1])
2352 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002353 remote, remote_branch = self.GetRemoteBranch()
2354 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01002355 pending_prefix_check=True,
2356 remote_url=self.GetRemoteUrl())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002357 if target_ref:
2358 upload_args.extend(['--target_ref', target_ref])
2359
2360 # Look for dependent patchsets. See crbug.com/480453 for more details.
2361 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2362 upstream_branch = ShortBranchName(upstream_branch)
2363 if remote is '.':
2364 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002365 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002366 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002367 print()
2368 print('Skipping dependency patchset upload because git config '
2369 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2370 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002371 else:
2372 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002373 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002374 auth_config=auth_config)
2375 branch_cl_issue_url = branch_cl.GetIssueURL()
2376 branch_cl_issue = branch_cl.GetIssue()
2377 branch_cl_patchset = branch_cl.GetPatchset()
2378 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2379 upload_args.extend(
2380 ['--depends_on_patchset', '%s:%s' % (
2381 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002382 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002383 '\n'
2384 'The current branch (%s) is tracking a local branch (%s) with '
2385 'an associated CL.\n'
2386 'Adding %s/#ps%s as a dependency patchset.\n'
2387 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2388 branch_cl_patchset))
2389
2390 project = settings.GetProject()
2391 if project:
2392 upload_args.extend(['--project', project])
2393
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002394 try:
2395 upload_args = ['upload'] + upload_args + args
2396 logging.info('upload.RealMain(%s)', upload_args)
2397 issue, patchset = upload.RealMain(upload_args)
2398 issue = int(issue)
2399 patchset = int(patchset)
2400 except KeyboardInterrupt:
2401 sys.exit(1)
2402 except:
2403 # If we got an exception after the user typed a description for their
2404 # change, back up the description before re-raising.
2405 if change_desc:
2406 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2407 print('\nGot exception while uploading -- saving description to %s\n' %
2408 backup_path)
2409 backup_file = open(backup_path, 'w')
2410 backup_file.write(change_desc.description)
2411 backup_file.close()
2412 raise
2413
2414 if not self.GetIssue():
2415 self.SetIssue(issue)
2416 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002417 return 0
2418
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002419
2420class _GerritChangelistImpl(_ChangelistCodereviewBase):
2421 def __init__(self, changelist, auth_config=None):
2422 # auth_config is Rietveld thing, kept here to preserve interface only.
2423 super(_GerritChangelistImpl, self).__init__(changelist)
2424 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002425 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002426 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002427 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002428
2429 def _GetGerritHost(self):
2430 # Lazy load of configs.
2431 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002432 if self._gerrit_host and '.' not in self._gerrit_host:
2433 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2434 # This happens for internal stuff http://crbug.com/614312.
2435 parsed = urlparse.urlparse(self.GetRemoteUrl())
2436 if parsed.scheme == 'sso':
2437 print('WARNING: using non https URLs for remote is likely broken\n'
2438 ' Your current remote is: %s' % self.GetRemoteUrl())
2439 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2440 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002441 return self._gerrit_host
2442
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002443 def _GetGitHost(self):
2444 """Returns git host to be used when uploading change to Gerrit."""
2445 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2446
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002447 def GetCodereviewServer(self):
2448 if not self._gerrit_server:
2449 # If we're on a branch then get the server potentially associated
2450 # with that branch.
2451 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002452 self._gerrit_server = self._GitGetBranchConfigValue(
2453 self.CodereviewServerConfigKey())
2454 if self._gerrit_server:
2455 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002456 if not self._gerrit_server:
2457 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2458 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002459 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002460 parts[0] = parts[0] + '-review'
2461 self._gerrit_host = '.'.join(parts)
2462 self._gerrit_server = 'https://%s' % self._gerrit_host
2463 return self._gerrit_server
2464
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002465 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002466 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002467 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002468
tandrii5d48c322016-08-18 16:19:37 -07002469 @classmethod
2470 def PatchsetConfigKey(cls):
2471 return 'gerritpatchset'
2472
2473 @classmethod
2474 def CodereviewServerConfigKey(cls):
2475 return 'gerritserver'
2476
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002477 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002478 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002479 if settings.GetGerritSkipEnsureAuthenticated():
2480 # For projects with unusual authentication schemes.
2481 # See http://crbug.com/603378.
2482 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002483 # Lazy-loader to identify Gerrit and Git hosts.
2484 if gerrit_util.GceAuthenticator.is_gce():
2485 return
2486 self.GetCodereviewServer()
2487 git_host = self._GetGitHost()
2488 assert self._gerrit_server and self._gerrit_host
2489 cookie_auth = gerrit_util.CookiesAuthenticator()
2490
2491 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2492 git_auth = cookie_auth.get_auth_header(git_host)
2493 if gerrit_auth and git_auth:
2494 if gerrit_auth == git_auth:
2495 return
2496 print((
2497 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2498 ' Check your %s or %s file for credentials of hosts:\n'
2499 ' %s\n'
2500 ' %s\n'
2501 ' %s') %
2502 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2503 git_host, self._gerrit_host,
2504 cookie_auth.get_new_password_message(git_host)))
2505 if not force:
2506 ask_for_data('If you know what you are doing, press Enter to continue, '
2507 'Ctrl+C to abort.')
2508 return
2509 else:
2510 missing = (
2511 [] if gerrit_auth else [self._gerrit_host] +
2512 [] if git_auth else [git_host])
2513 DieWithError('Credentials for the following hosts are required:\n'
2514 ' %s\n'
2515 'These are read from %s (or legacy %s)\n'
2516 '%s' % (
2517 '\n '.join(missing),
2518 cookie_auth.get_gitcookies_path(),
2519 cookie_auth.get_netrc_path(),
2520 cookie_auth.get_new_password_message(git_host)))
2521
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002522 def _PostUnsetIssueProperties(self):
2523 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002524 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002525
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002526 def GetRieveldObjForPresubmit(self):
2527 class ThisIsNotRietveldIssue(object):
2528 def __nonzero__(self):
2529 # This is a hack to make presubmit_support think that rietveld is not
2530 # defined, yet still ensure that calls directly result in a decent
2531 # exception message below.
2532 return False
2533
2534 def __getattr__(self, attr):
2535 print(
2536 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2537 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2538 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2539 'or use Rietveld for codereview.\n'
2540 'See also http://crbug.com/579160.' % attr)
2541 raise NotImplementedError()
2542 return ThisIsNotRietveldIssue()
2543
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002544 def GetGerritObjForPresubmit(self):
2545 return presubmit_support.GerritAccessor(self._GetGerritHost())
2546
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002547 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002548 """Apply a rough heuristic to give a simple summary of an issue's review
2549 or CQ status, assuming adherence to a common workflow.
2550
2551 Returns None if no issue for this branch, or one of the following keywords:
2552 * 'error' - error from review tool (including deleted issues)
2553 * 'unsent' - no reviewers added
2554 * 'waiting' - waiting for review
2555 * 'reply' - waiting for owner to reply to review
Quinten Yearsley442fb642016-12-15 15:38:27 -08002556 * 'not lgtm' - Code-Review disapproval from at least one valid reviewer
tandriic2405f52016-10-10 08:13:15 -07002557 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002558 * 'commit' - in the commit queue
2559 * 'closed' - abandoned
2560 """
2561 if not self.GetIssue():
2562 return None
2563
2564 try:
2565 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002566 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002567 return 'error'
2568
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002569 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002570 return 'closed'
2571
2572 cq_label = data['labels'].get('Commit-Queue', {})
2573 if cq_label:
rmistryc9ebbd22016-10-14 12:35:54 -07002574 votes = cq_label.get('all', [])
2575 highest_vote = 0
2576 for v in votes:
2577 highest_vote = max(highest_vote, v.get('value', 0))
2578 vote_value = str(highest_vote)
2579 if vote_value != '0':
2580 # Add a '+' if the value is not 0 to match the values in the label.
2581 # The cq_label does not have negatives.
2582 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002583 vote_text = cq_label.get('values', {}).get(vote_value, '')
2584 if vote_text.lower() == 'commit':
2585 return 'commit'
2586
2587 lgtm_label = data['labels'].get('Code-Review', {})
2588 if lgtm_label:
2589 if 'rejected' in lgtm_label:
2590 return 'not lgtm'
2591 if 'approved' in lgtm_label:
2592 return 'lgtm'
2593
2594 if not data.get('reviewers', {}).get('REVIEWER', []):
2595 return 'unsent'
2596
2597 messages = data.get('messages', [])
2598 if messages:
2599 owner = data['owner'].get('_account_id')
2600 last_message_author = messages[-1].get('author', {}).get('_account_id')
2601 if owner != last_message_author:
2602 # Some reply from non-owner.
2603 return 'reply'
2604
2605 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002606
2607 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002608 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002609 return data['revisions'][data['current_revision']]['_number']
2610
2611 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002612 data = self._GetChangeDetail(['CURRENT_REVISION'])
2613 current_rev = data['current_revision']
2614 url = data['revisions'][current_rev]['fetch']['http']['url']
2615 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002616
dsansomee2d6fd92016-09-08 00:10:47 -07002617 def UpdateDescriptionRemote(self, description, force=False):
2618 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2619 if not force:
2620 ask_for_data(
2621 'The description cannot be modified while the issue has a pending '
2622 'unpublished edit. Either publish the edit in the Gerrit web UI '
2623 'or delete it.\n\n'
2624 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2625
2626 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2627 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002628 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002629 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002630
2631 def CloseIssue(self):
2632 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2633
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002634 def GetApprovingReviewers(self):
2635 """Returns a list of reviewers approving the change.
2636
2637 Note: not necessarily committers.
2638 """
2639 raise NotImplementedError()
2640
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002641 def SubmitIssue(self, wait_for_merge=True):
2642 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2643 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002644
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002645 def _GetChangeDetail(self, options=None, issue=None):
2646 options = options or []
2647 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002648 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002649 try:
2650 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2651 options, ignore_404=False)
2652 except gerrit_util.GerritError as e:
2653 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002654 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002655 raise
tandriic2405f52016-10-10 08:13:15 -07002656 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002657
agable32978d92016-11-01 12:55:02 -07002658 def _GetChangeCommit(self, issue=None):
2659 issue = issue or self.GetIssue()
2660 assert issue, 'issue is required to query Gerrit'
2661 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2662 if not data:
Aaron Gablea45ee112016-11-22 15:14:38 -08002663 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
agable32978d92016-11-01 12:55:02 -07002664 return data
2665
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002666 def CMDLand(self, force, bypass_hooks, verbose):
2667 if git_common.is_dirty_git_tree('land'):
2668 return 1
tandriid60367b2016-06-22 05:25:12 -07002669 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2670 if u'Commit-Queue' in detail.get('labels', {}):
2671 if not force:
2672 ask_for_data('\nIt seems this repository has a Commit Queue, '
2673 'which can test and land changes for you. '
2674 'Are you sure you wish to bypass it?\n'
2675 'Press Enter to continue, Ctrl+C to abort.')
2676
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002677 differs = True
tandriic4344b52016-08-29 06:04:54 -07002678 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002679 # Note: git diff outputs nothing if there is no diff.
2680 if not last_upload or RunGit(['diff', last_upload]).strip():
2681 print('WARNING: some changes from local branch haven\'t been uploaded')
2682 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002683 if detail['current_revision'] == last_upload:
2684 differs = False
2685 else:
2686 print('WARNING: local branch contents differ from latest uploaded '
2687 'patchset')
2688 if differs:
2689 if not force:
2690 ask_for_data(
Andrii Shyshkalov3aac7752016-11-16 15:23:13 +01002691 'Do you want to submit latest Gerrit patchset and bypass hooks?\n'
2692 'Press Enter to continue, Ctrl+C to abort.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002693 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2694 elif not bypass_hooks:
2695 hook_results = self.RunHook(
2696 committing=True,
2697 may_prompt=not force,
2698 verbose=verbose,
2699 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2700 if not hook_results.should_continue():
2701 return 1
2702
2703 self.SubmitIssue(wait_for_merge=True)
2704 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002705 links = self._GetChangeCommit().get('web_links', [])
2706 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002707 if link.get('name') == 'gitiles' and link.get('url'):
agable32978d92016-11-01 12:55:02 -07002708 print('Landed as %s' % link.get('url'))
2709 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002710 return 0
2711
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002712 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2713 directory):
2714 assert not reject
2715 assert not nocommit
2716 assert not directory
2717 assert parsed_issue_arg.valid
2718
2719 self._changelist.issue = parsed_issue_arg.issue
2720
2721 if parsed_issue_arg.hostname:
2722 self._gerrit_host = parsed_issue_arg.hostname
2723 self._gerrit_server = 'https://%s' % self._gerrit_host
2724
tandriic2405f52016-10-10 08:13:15 -07002725 try:
2726 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002727 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002728 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002729
2730 if not parsed_issue_arg.patchset:
2731 # Use current revision by default.
2732 revision_info = detail['revisions'][detail['current_revision']]
2733 patchset = int(revision_info['_number'])
2734 else:
2735 patchset = parsed_issue_arg.patchset
2736 for revision_info in detail['revisions'].itervalues():
2737 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2738 break
2739 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002740 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002741 (parsed_issue_arg.patchset, self.GetIssue()))
2742
2743 fetch_info = revision_info['fetch']['http']
2744 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2745 RunGit(['cherry-pick', 'FETCH_HEAD'])
2746 self.SetIssue(self.GetIssue())
2747 self.SetPatchset(patchset)
Aaron Gablea45ee112016-11-22 15:14:38 -08002748 print('Committed patch for change %i patchset %i locally' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002749 (self.GetIssue(), self.GetPatchset()))
2750 return 0
2751
2752 @staticmethod
2753 def ParseIssueURL(parsed_url):
2754 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2755 return None
2756 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2757 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2758 # Short urls like https://domain/<issue_number> can be used, but don't allow
2759 # specifying the patchset (you'd 404), but we allow that here.
2760 if parsed_url.path == '/':
2761 part = parsed_url.fragment
2762 else:
2763 part = parsed_url.path
2764 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2765 if match:
2766 return _ParsedIssueNumberArgument(
2767 issue=int(match.group(2)),
2768 patchset=int(match.group(4)) if match.group(4) else None,
2769 hostname=parsed_url.netloc)
2770 return None
2771
tandrii16e0b4e2016-06-07 10:34:28 -07002772 def _GerritCommitMsgHookCheck(self, offer_removal):
2773 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2774 if not os.path.exists(hook):
2775 return
2776 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2777 # custom developer made one.
2778 data = gclient_utils.FileRead(hook)
2779 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2780 return
2781 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002782 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002783 'and may interfere with it in subtle ways.\n'
2784 'We recommend you remove the commit-msg hook.')
2785 if offer_removal:
2786 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2787 if reply.lower().startswith('y'):
2788 gclient_utils.rm_file_or_tree(hook)
2789 print('Gerrit commit-msg hook removed.')
2790 else:
2791 print('OK, will keep Gerrit commit-msg hook in place.')
2792
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002793 def CMDUploadChange(self, options, args, change):
2794 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002795 if options.squash and options.no_squash:
2796 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002797
2798 if not options.squash and not options.no_squash:
2799 # Load default for user, repo, squash=true, in this order.
2800 options.squash = settings.GetSquashGerritUploads()
2801 elif options.no_squash:
2802 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002803
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002804 # We assume the remote called "origin" is the one we want.
2805 # It is probably not worthwhile to support different workflows.
2806 gerrit_remote = 'origin'
2807
2808 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01002809 # Gerrit will not support pending prefix at all.
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002810 branch = GetTargetRef(remote, remote_branch, options.target_branch,
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01002811 pending_prefix_check=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002812
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002813 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002814 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002815 if self.GetIssue():
2816 # Try to get the message from a previous upload.
2817 message = self.GetDescription()
2818 if not message:
2819 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002820 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002821 '%s' % (self.GetIssue(), self.GetIssueURL()))
2822 change_id = self._GetChangeDetail()['change_id']
2823 while True:
2824 footer_change_ids = git_footers.get_footer_change_id(message)
2825 if footer_change_ids == [change_id]:
2826 break
2827 if not footer_change_ids:
2828 message = git_footers.add_footer_change_id(message, change_id)
Aaron Gablea45ee112016-11-22 15:14:38 -08002829 print('WARNING: appended missing Change-Id to change description')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002830 continue
2831 # There is already a valid footer but with different or several ids.
2832 # Doing this automatically is non-trivial as we don't want to lose
2833 # existing other footers, yet we want to append just 1 desired
2834 # Change-Id. Thus, just create a new footer, but let user verify the
2835 # new description.
2836 message = '%s\n\nChange-Id: %s' % (message, change_id)
2837 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002838 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002839 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002840 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002841 'Please, check the proposed correction to the description, '
2842 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2843 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2844 change_id))
2845 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2846 if not options.force:
2847 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002848 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002849 message = change_desc.description
2850 if not message:
2851 DieWithError("Description is empty. Aborting...")
2852 # Continue the while loop.
2853 # Sanity check of this code - we should end up with proper message
2854 # footer.
2855 assert [change_id] == git_footers.get_footer_change_id(message)
2856 change_desc = ChangeDescription(message)
2857 else:
2858 change_desc = ChangeDescription(
2859 options.message or CreateDescriptionFromLog(args))
2860 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002861 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002862 if not change_desc.description:
2863 DieWithError("Description is empty. Aborting...")
2864 message = change_desc.description
2865 change_ids = git_footers.get_footer_change_id(message)
2866 if len(change_ids) > 1:
2867 DieWithError('too many Change-Id footers, at most 1 allowed.')
2868 if not change_ids:
2869 # Generate the Change-Id automatically.
2870 message = git_footers.add_footer_change_id(
2871 message, GenerateGerritChangeId(message))
2872 change_desc.set_description(message)
2873 change_ids = git_footers.get_footer_change_id(message)
2874 assert len(change_ids) == 1
2875 change_id = change_ids[0]
2876
2877 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2878 if remote is '.':
2879 # If our upstream branch is local, we base our squashed commit on its
2880 # squashed version.
2881 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2882 # Check the squashed hash of the parent.
2883 parent = RunGit(['config',
2884 'branch.%s.gerritsquashhash' % upstream_branch_name],
2885 error_ok=True).strip()
2886 # Verify that the upstream branch has been uploaded too, otherwise
2887 # Gerrit will create additional CLs when uploading.
2888 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2889 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002890 DieWithError(
2891 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002892 'Note: maybe you\'ve uploaded it with --no-squash. '
2893 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002894 ' git cl upload --squash\n' % upstream_branch_name)
2895 else:
2896 parent = self.GetCommonAncestorWithUpstream()
2897
2898 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2899 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2900 '-m', message]).strip()
2901 else:
2902 change_desc = ChangeDescription(
2903 options.message or CreateDescriptionFromLog(args))
2904 if not change_desc.description:
2905 DieWithError("Description is empty. Aborting...")
2906
2907 if not git_footers.get_footer_change_id(change_desc.description):
2908 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002909 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2910 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002911 ref_to_push = 'HEAD'
2912 parent = '%s/%s' % (gerrit_remote, branch)
2913 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2914
2915 assert change_desc
2916 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2917 ref_to_push)]).splitlines()
2918 if len(commits) > 1:
2919 print('WARNING: This will upload %d commits. Run the following command '
2920 'to see which commits will be uploaded: ' % len(commits))
2921 print('git log %s..%s' % (parent, ref_to_push))
2922 print('You can also use `git squash-branch` to squash these into a '
2923 'single commit.')
2924 ask_for_data('About to upload; enter to confirm.')
2925
2926 if options.reviewers or options.tbr_owners:
2927 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2928 change)
2929
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002930 # Extra options that can be specified at push time. Doc:
2931 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2932 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002933 if change_desc.get_reviewers(tbr_only=True):
2934 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2935 refspec_opts.append('l=Code-Review+1')
2936
Aaron Gable9b713dd2016-12-14 16:04:21 -08002937 title = options.title
2938 if not title:
2939 if self.GetIssue():
2940 # We already have an issue, so we should ask for a title for new patch.
2941 default = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
2942 title = ask_for_data('Title for patchset [%s]: ' % default) or default
2943 else:
2944 title = 'Initial upload'
2945 if title:
2946 if not re.match(r'^[\w ]+$', title):
2947 title = re.sub(r'[^\w ]', '', title)
tandriieefe8322016-08-17 10:12:24 -07002948 print('WARNING: Patchset title may only contain alphanumeric chars '
Aaron Gable9b713dd2016-12-14 16:04:21 -08002949 'and spaces. Cleaned up title:\n%s' % title)
tandriieefe8322016-08-17 10:12:24 -07002950 if not options.force:
2951 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002952 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2953 # reverse on its side.
Aaron Gable9b713dd2016-12-14 16:04:21 -08002954 refspec_opts.append('m=' + title.replace(' ', '_'))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002955
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002956 if options.send_mail:
2957 if not change_desc.get_reviewers():
2958 DieWithError('Must specify reviewers to send email.')
2959 refspec_opts.append('notify=ALL')
2960 else:
2961 refspec_opts.append('notify=NONE')
2962
tandrii99a72f22016-08-17 14:33:24 -07002963 reviewers = change_desc.get_reviewers()
2964 if reviewers:
2965 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002966
agablec6787972016-09-09 16:13:34 -07002967 if options.private:
2968 refspec_opts.append('draft')
2969
rmistry9eadede2016-09-19 11:22:43 -07002970 if options.topic:
2971 # Documentation on Gerrit topics is here:
2972 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2973 refspec_opts.append('topic=%s' % options.topic)
2974
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002975 refspec_suffix = ''
2976 if refspec_opts:
2977 refspec_suffix = '%' + ','.join(refspec_opts)
2978 assert ' ' not in refspec_suffix, (
2979 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002980 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002981
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002982 try:
2983 push_stdout = gclient_utils.CheckCallAndFilter(
2984 ['git', 'push', gerrit_remote, refspec],
2985 print_stdout=True,
2986 # Flush after every line: useful for seeing progress when running as
2987 # recipe.
2988 filter_fn=lambda _: sys.stdout.flush())
2989 except subprocess2.CalledProcessError:
2990 DieWithError('Failed to create a change. Please examine output above '
2991 'for the reason of the failure. ')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002992
2993 if options.squash:
2994 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2995 change_numbers = [m.group(1)
2996 for m in map(regex.match, push_stdout.splitlines())
2997 if m]
2998 if len(change_numbers) != 1:
2999 DieWithError(
3000 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
3001 'Change-Id: %s') % (len(change_numbers), change_id))
3002 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07003003 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07003004
3005 # Add cc's from the CC_LIST and --cc flag (if any).
3006 cc = self.GetCCList().split(',')
3007 if options.cc:
3008 cc.extend(options.cc)
3009 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07003010 if change_desc.get_cced():
3011 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07003012 if cc:
Aaron Gable5db82092016-11-17 10:52:08 -08003013 gerrit_util.AddReviewers(
tandrii88189772016-09-29 04:29:57 -07003014 self._GetGerritHost(), self.GetIssue(), cc, is_reviewer=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003015 return 0
3016
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003017 def _AddChangeIdToCommitMessage(self, options, args):
3018 """Re-commits using the current message, assumes the commit hook is in
3019 place.
3020 """
3021 log_desc = options.message or CreateDescriptionFromLog(args)
3022 git_command = ['commit', '--amend', '-m', log_desc]
3023 RunGit(git_command)
3024 new_log_desc = CreateDescriptionFromLog(args)
3025 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003026 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003027 return new_log_desc
3028 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00003029 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003030
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003031 def SetCQState(self, new_state):
3032 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003033 vote_map = {
3034 _CQState.NONE: 0,
3035 _CQState.DRY_RUN: 1,
3036 _CQState.COMMIT : 2,
3037 }
Andrii Shyshkalov828701b2016-12-09 10:46:47 +01003038 kwargs = {'labels': {'Commit-Queue': vote_map[new_state]}}
3039 if new_state == _CQState.DRY_RUN:
3040 # Don't spam everybody reviewer/owner.
3041 kwargs['notify'] = 'NONE'
3042 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(), **kwargs)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003043
tandriie113dfd2016-10-11 10:20:12 -07003044 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003045 try:
3046 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003047 except GerritChangeNotExists:
3048 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003049
3050 if data['status'] in ('ABANDONED', 'MERGED'):
3051 return 'CL %s is closed' % self.GetIssue()
3052
3053 def GetTryjobProperties(self, patchset=None):
3054 """Returns dictionary of properties to launch tryjob."""
3055 data = self._GetChangeDetail(['ALL_REVISIONS'])
3056 patchset = int(patchset or self.GetPatchset())
3057 assert patchset
3058 revision_data = None # Pylint wants it to be defined.
3059 for revision_data in data['revisions'].itervalues():
3060 if int(revision_data['_number']) == patchset:
3061 break
3062 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003063 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003064 (patchset, self.GetIssue()))
3065 return {
3066 'patch_issue': self.GetIssue(),
3067 'patch_set': patchset or self.GetPatchset(),
3068 'patch_project': data['project'],
3069 'patch_storage': 'gerrit',
3070 'patch_ref': revision_data['fetch']['http']['ref'],
3071 'patch_repository_url': revision_data['fetch']['http']['url'],
3072 'patch_gerrit_url': self.GetCodereviewServer(),
3073 }
tandriie113dfd2016-10-11 10:20:12 -07003074
tandriide281ae2016-10-12 06:02:30 -07003075 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003076 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003077
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003078
3079_CODEREVIEW_IMPLEMENTATIONS = {
3080 'rietveld': _RietveldChangelistImpl,
3081 'gerrit': _GerritChangelistImpl,
3082}
3083
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003084
iannuccie53c9352016-08-17 14:40:40 -07003085def _add_codereview_issue_select_options(parser, extra=""):
3086 _add_codereview_select_options(parser)
3087
3088 text = ('Operate on this issue number instead of the current branch\'s '
3089 'implicit issue.')
3090 if extra:
3091 text += ' '+extra
3092 parser.add_option('-i', '--issue', type=int, help=text)
3093
3094
3095def _process_codereview_issue_select_options(parser, options):
3096 _process_codereview_select_options(parser, options)
3097 if options.issue is not None and not options.forced_codereview:
3098 parser.error('--issue must be specified with either --rietveld or --gerrit')
3099
3100
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003101def _add_codereview_select_options(parser):
3102 """Appends --gerrit and --rietveld options to force specific codereview."""
3103 parser.codereview_group = optparse.OptionGroup(
3104 parser, 'EXPERIMENTAL! Codereview override options')
3105 parser.add_option_group(parser.codereview_group)
3106 parser.codereview_group.add_option(
3107 '--gerrit', action='store_true',
3108 help='Force the use of Gerrit for codereview')
3109 parser.codereview_group.add_option(
3110 '--rietveld', action='store_true',
3111 help='Force the use of Rietveld for codereview')
3112
3113
3114def _process_codereview_select_options(parser, options):
3115 if options.gerrit and options.rietveld:
3116 parser.error('Options --gerrit and --rietveld are mutually exclusive')
3117 options.forced_codereview = None
3118 if options.gerrit:
3119 options.forced_codereview = 'gerrit'
3120 elif options.rietveld:
3121 options.forced_codereview = 'rietveld'
3122
3123
tandriif9aefb72016-07-01 09:06:51 -07003124def _get_bug_line_values(default_project, bugs):
3125 """Given default_project and comma separated list of bugs, yields bug line
3126 values.
3127
3128 Each bug can be either:
3129 * a number, which is combined with default_project
3130 * string, which is left as is.
3131
3132 This function may produce more than one line, because bugdroid expects one
3133 project per line.
3134
3135 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3136 ['v8:123', 'chromium:789']
3137 """
3138 default_bugs = []
3139 others = []
3140 for bug in bugs.split(','):
3141 bug = bug.strip()
3142 if bug:
3143 try:
3144 default_bugs.append(int(bug))
3145 except ValueError:
3146 others.append(bug)
3147
3148 if default_bugs:
3149 default_bugs = ','.join(map(str, default_bugs))
3150 if default_project:
3151 yield '%s:%s' % (default_project, default_bugs)
3152 else:
3153 yield default_bugs
3154 for other in sorted(others):
3155 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3156 yield other
3157
3158
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003159class ChangeDescription(object):
3160 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003161 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003162 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00003163 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003164 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003165
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003166 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003167 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003168
agable@chromium.org42c20792013-09-12 17:34:49 +00003169 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003170 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003171 return '\n'.join(self._description_lines)
3172
3173 def set_description(self, desc):
3174 if isinstance(desc, basestring):
3175 lines = desc.splitlines()
3176 else:
3177 lines = [line.rstrip() for line in desc]
3178 while lines and not lines[0]:
3179 lines.pop(0)
3180 while lines and not lines[-1]:
3181 lines.pop(-1)
3182 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003183
piman@chromium.org336f9122014-09-04 02:16:55 +00003184 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003185 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003186 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00003187 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003188 return
agable@chromium.org42c20792013-09-12 17:34:49 +00003189 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003190
agable@chromium.org42c20792013-09-12 17:34:49 +00003191 # Get the set of R= and TBR= lines and remove them from the desciption.
3192 regexp = re.compile(self.R_LINE)
3193 matches = [regexp.match(line) for line in self._description_lines]
3194 new_desc = [l for i, l in enumerate(self._description_lines)
3195 if not matches[i]]
3196 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003197
agable@chromium.org42c20792013-09-12 17:34:49 +00003198 # Construct new unified R= and TBR= lines.
3199 r_names = []
3200 tbr_names = []
3201 for match in matches:
3202 if not match:
3203 continue
3204 people = cleanup_list([match.group(2).strip()])
3205 if match.group(1) == 'TBR':
3206 tbr_names.extend(people)
3207 else:
3208 r_names.extend(people)
3209 for name in r_names:
3210 if name not in reviewers:
3211 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003212 if add_owners_tbr:
3213 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003214 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003215 all_reviewers = set(tbr_names + reviewers)
3216 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3217 all_reviewers)
3218 tbr_names.extend(owners_db.reviewers_for(missing_files,
3219 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003220 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3221 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3222
3223 # Put the new lines in the description where the old first R= line was.
3224 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3225 if 0 <= line_loc < len(self._description_lines):
3226 if new_tbr_line:
3227 self._description_lines.insert(line_loc, new_tbr_line)
3228 if new_r_line:
3229 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003230 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003231 if new_r_line:
3232 self.append_footer(new_r_line)
3233 if new_tbr_line:
3234 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003235
tandriif9aefb72016-07-01 09:06:51 -07003236 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003237 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003238 self.set_description([
3239 '# Enter a description of the change.',
3240 '# This will be displayed on the codereview site.',
3241 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003242 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003243 '--------------------',
3244 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003245
agable@chromium.org42c20792013-09-12 17:34:49 +00003246 regexp = re.compile(self.BUG_LINE)
3247 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003248 prefix = settings.GetBugPrefix()
3249 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
3250 for value in values:
3251 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
3252 self.append_footer('BUG=%s' % value)
3253
agable@chromium.org42c20792013-09-12 17:34:49 +00003254 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003255 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003256 if not content:
3257 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003258 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003259
3260 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003261 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3262 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003263 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003264 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003265
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003266 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003267 """Adds a footer line to the description.
3268
3269 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3270 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3271 that Gerrit footers are always at the end.
3272 """
3273 parsed_footer_line = git_footers.parse_footer(line)
3274 if parsed_footer_line:
3275 # Line is a gerrit footer in the form: Footer-Key: any value.
3276 # Thus, must be appended observing Gerrit footer rules.
3277 self.set_description(
3278 git_footers.add_footer(self.description,
3279 key=parsed_footer_line[0],
3280 value=parsed_footer_line[1]))
3281 return
3282
3283 if not self._description_lines:
3284 self._description_lines.append(line)
3285 return
3286
3287 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3288 if gerrit_footers:
3289 # git_footers.split_footers ensures that there is an empty line before
3290 # actual (gerrit) footers, if any. We have to keep it that way.
3291 assert top_lines and top_lines[-1] == ''
3292 top_lines, separator = top_lines[:-1], top_lines[-1:]
3293 else:
3294 separator = [] # No need for separator if there are no gerrit_footers.
3295
3296 prev_line = top_lines[-1] if top_lines else ''
3297 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3298 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3299 top_lines.append('')
3300 top_lines.append(line)
3301 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003302
tandrii99a72f22016-08-17 14:33:24 -07003303 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003304 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003305 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003306 reviewers = [match.group(2).strip()
3307 for match in matches
3308 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003309 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003310
bradnelsond975b302016-10-23 12:20:23 -07003311 def get_cced(self):
3312 """Retrieves the list of reviewers."""
3313 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3314 cced = [match.group(2).strip() for match in matches if match]
3315 return cleanup_list(cced)
3316
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003317 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3318 """Updates this commit description given the parent.
3319
3320 This is essentially what Gnumbd used to do.
3321 Consult https://goo.gl/WMmpDe for more details.
3322 """
3323 assert parent_msg # No, orphan branch creation isn't supported.
3324 assert parent_hash
3325 assert dest_ref
3326 parent_footer_map = git_footers.parse_footers(parent_msg)
3327 # This will also happily parse svn-position, which GnumbD is no longer
3328 # supporting. While we'd generate correct footers, the verifier plugin
3329 # installed in Gerrit will block such commit (ie git push below will fail).
3330 parent_position = git_footers.get_position(parent_footer_map)
3331
3332 # Cherry-picks may have last line obscuring their prior footers,
3333 # from git_footers perspective. This is also what Gnumbd did.
3334 cp_line = None
3335 if (self._description_lines and
3336 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3337 cp_line = self._description_lines.pop()
3338
3339 top_lines, _, parsed_footers = git_footers.split_footers(self.description)
3340
3341 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3342 # user interference with actual footers we'd insert below.
3343 for i, (k, v) in enumerate(parsed_footers):
3344 if k.startswith('Cr-'):
3345 parsed_footers[i] = (k.replace('Cr-', 'Cr-Original-'), v)
3346
3347 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003348 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003349 if parent_position[0] == dest_ref:
3350 # Same branch as parent.
3351 number = int(parent_position[1]) + 1
3352 else:
3353 number = 1 # New branch, and extra lineage.
3354 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3355 int(parent_position[1])))
3356
3357 parsed_footers.append(('Cr-Commit-Position',
3358 '%s@{#%d}' % (dest_ref, number)))
3359 parsed_footers.extend(('Cr-Branched-From', v) for v in lineage)
3360
3361 self._description_lines = top_lines
3362 if cp_line:
3363 self._description_lines.append(cp_line)
3364 if self._description_lines[-1] != '':
3365 self._description_lines.append('') # Ensure footer separator.
3366 self._description_lines.extend('%s: %s' % kv for kv in parsed_footers)
3367
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003368
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003369def get_approving_reviewers(props):
3370 """Retrieves the reviewers that approved a CL from the issue properties with
3371 messages.
3372
3373 Note that the list may contain reviewers that are not committer, thus are not
3374 considered by the CQ.
3375 """
3376 return sorted(
3377 set(
3378 message['sender']
3379 for message in props['messages']
3380 if message['approval'] and message['sender'] in props['reviewers']
3381 )
3382 )
3383
3384
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003385def FindCodereviewSettingsFile(filename='codereview.settings'):
3386 """Finds the given file starting in the cwd and going up.
3387
3388 Only looks up to the top of the repository unless an
3389 'inherit-review-settings-ok' file exists in the root of the repository.
3390 """
3391 inherit_ok_file = 'inherit-review-settings-ok'
3392 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003393 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003394 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3395 root = '/'
3396 while True:
3397 if filename in os.listdir(cwd):
3398 if os.path.isfile(os.path.join(cwd, filename)):
3399 return open(os.path.join(cwd, filename))
3400 if cwd == root:
3401 break
3402 cwd = os.path.dirname(cwd)
3403
3404
3405def LoadCodereviewSettingsFromFile(fileobj):
3406 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003407 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003408
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003409 def SetProperty(name, setting, unset_error_ok=False):
3410 fullname = 'rietveld.' + name
3411 if setting in keyvals:
3412 RunGit(['config', fullname, keyvals[setting]])
3413 else:
3414 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3415
tandrii48df5812016-10-17 03:55:37 -07003416 if not keyvals.get('GERRIT_HOST', False):
3417 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003418 # Only server setting is required. Other settings can be absent.
3419 # In that case, we ignore errors raised during option deletion attempt.
3420 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003421 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003422 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3423 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003424 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003425 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003426 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
3427 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003428 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003429 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003430 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003431 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3432 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003433
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003434 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003435 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003436
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003437 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003438 RunGit(['config', 'gerrit.squash-uploads',
3439 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003440
tandrii@chromium.org28253532016-04-14 13:46:56 +00003441 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003442 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003443 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3444
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003445 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3446 #should be of the form
3447 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3448 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
3449 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3450 keyvals['ORIGIN_URL_CONFIG']])
3451
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003452
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003453def urlretrieve(source, destination):
3454 """urllib is broken for SSL connections via a proxy therefore we
3455 can't use urllib.urlretrieve()."""
3456 with open(destination, 'w') as f:
3457 f.write(urllib2.urlopen(source).read())
3458
3459
ukai@chromium.org712d6102013-11-27 00:52:58 +00003460def hasSheBang(fname):
3461 """Checks fname is a #! script."""
3462 with open(fname) as f:
3463 return f.read(2).startswith('#!')
3464
3465
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003466# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3467def DownloadHooks(*args, **kwargs):
3468 pass
3469
3470
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003471def DownloadGerritHook(force):
3472 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003473
3474 Args:
3475 force: True to update hooks. False to install hooks if not present.
3476 """
3477 if not settings.GetIsGerrit():
3478 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003479 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003480 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3481 if not os.access(dst, os.X_OK):
3482 if os.path.exists(dst):
3483 if not force:
3484 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003485 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003486 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003487 if not hasSheBang(dst):
3488 DieWithError('Not a script: %s\n'
3489 'You need to download from\n%s\n'
3490 'into .git/hooks/commit-msg and '
3491 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003492 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3493 except Exception:
3494 if os.path.exists(dst):
3495 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003496 DieWithError('\nFailed to download hooks.\n'
3497 'You need to download from\n%s\n'
3498 'into .git/hooks/commit-msg and '
3499 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003500
3501
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003502
3503def GetRietveldCodereviewSettingsInteractively():
3504 """Prompt the user for settings."""
3505 server = settings.GetDefaultServerUrl(error_ok=True)
3506 prompt = 'Rietveld server (host[:port])'
3507 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3508 newserver = ask_for_data(prompt + ':')
3509 if not server and not newserver:
3510 newserver = DEFAULT_SERVER
3511 if newserver:
3512 newserver = gclient_utils.UpgradeToHttps(newserver)
3513 if newserver != server:
3514 RunGit(['config', 'rietveld.server', newserver])
3515
3516 def SetProperty(initial, caption, name, is_url):
3517 prompt = caption
3518 if initial:
3519 prompt += ' ("x" to clear) [%s]' % initial
3520 new_val = ask_for_data(prompt + ':')
3521 if new_val == 'x':
3522 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3523 elif new_val:
3524 if is_url:
3525 new_val = gclient_utils.UpgradeToHttps(new_val)
3526 if new_val != initial:
3527 RunGit(['config', 'rietveld.' + name, new_val])
3528
3529 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3530 SetProperty(settings.GetDefaultPrivateFlag(),
3531 'Private flag (rietveld only)', 'private', False)
3532 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3533 'tree-status-url', False)
3534 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3535 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3536 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3537 'run-post-upload-hook', False)
3538
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003539@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003540def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003541 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003542
tandrii5d0a0422016-09-14 06:24:35 -07003543 print('WARNING: git cl config works for Rietveld only')
3544 # TODO(tandrii): remove this once we switch to Gerrit.
3545 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003546 parser.add_option('--activate-update', action='store_true',
3547 help='activate auto-updating [rietveld] section in '
3548 '.git/config')
3549 parser.add_option('--deactivate-update', action='store_true',
3550 help='deactivate auto-updating [rietveld] section in '
3551 '.git/config')
3552 options, args = parser.parse_args(args)
3553
3554 if options.deactivate_update:
3555 RunGit(['config', 'rietveld.autoupdate', 'false'])
3556 return
3557
3558 if options.activate_update:
3559 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3560 return
3561
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003562 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003563 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003564 return 0
3565
3566 url = args[0]
3567 if not url.endswith('codereview.settings'):
3568 url = os.path.join(url, 'codereview.settings')
3569
3570 # Load code review settings and download hooks (if available).
3571 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3572 return 0
3573
3574
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003575def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003576 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003577 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3578 branch = ShortBranchName(branchref)
3579 _, args = parser.parse_args(args)
3580 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003581 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003582 return RunGit(['config', 'branch.%s.base-url' % branch],
3583 error_ok=False).strip()
3584 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003585 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003586 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3587 error_ok=False).strip()
3588
3589
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003590def color_for_status(status):
3591 """Maps a Changelist status to color, for CMDstatus and other tools."""
3592 return {
3593 'unsent': Fore.RED,
3594 'waiting': Fore.BLUE,
3595 'reply': Fore.YELLOW,
3596 'lgtm': Fore.GREEN,
3597 'commit': Fore.MAGENTA,
3598 'closed': Fore.CYAN,
3599 'error': Fore.WHITE,
3600 }.get(status, Fore.WHITE)
3601
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003602
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003603def get_cl_statuses(changes, fine_grained, max_processes=None):
3604 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003605
3606 If fine_grained is true, this will fetch CL statuses from the server.
3607 Otherwise, simply indicate if there's a matching url for the given branches.
3608
3609 If max_processes is specified, it is used as the maximum number of processes
3610 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3611 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003612
3613 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003614 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003615 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003616 upload.verbosity = 0
3617
3618 if fine_grained:
3619 # Process one branch synchronously to work through authentication, then
3620 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003621 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003622 def fetch(cl):
3623 try:
3624 return (cl, cl.GetStatus())
3625 except:
3626 # See http://crbug.com/629863.
3627 logging.exception('failed to fetch status for %s:', cl)
3628 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003629 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003630
tandriiea9514a2016-08-17 12:32:37 -07003631 changes_to_fetch = changes[1:]
3632 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003633 # Exit early if there was only one branch to fetch.
3634 return
3635
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003636 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003637 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003638 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003639 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003640
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003641 fetched_cls = set()
3642 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003643 while True:
3644 try:
3645 row = it.next(timeout=5)
3646 except multiprocessing.TimeoutError:
3647 break
3648
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003649 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003650 yield row
3651
3652 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003653 for cl in set(changes_to_fetch) - fetched_cls:
3654 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003655
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003656 else:
3657 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003658 for cl in changes:
3659 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003660
rmistry@google.com2dd99862015-06-22 12:22:18 +00003661
3662def upload_branch_deps(cl, args):
3663 """Uploads CLs of local branches that are dependents of the current branch.
3664
3665 If the local branch dependency tree looks like:
3666 test1 -> test2.1 -> test3.1
3667 -> test3.2
3668 -> test2.2 -> test3.3
3669
3670 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3671 run on the dependent branches in this order:
3672 test2.1, test3.1, test3.2, test2.2, test3.3
3673
3674 Note: This function does not rebase your local dependent branches. Use it when
3675 you make a change to the parent branch that will not conflict with its
3676 dependent branches, and you would like their dependencies updated in
3677 Rietveld.
3678 """
3679 if git_common.is_dirty_git_tree('upload-branch-deps'):
3680 return 1
3681
3682 root_branch = cl.GetBranch()
3683 if root_branch is None:
3684 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3685 'Get on a branch!')
3686 if not cl.GetIssue() or not cl.GetPatchset():
3687 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3688 'patchset dependencies without an uploaded CL.')
3689
3690 branches = RunGit(['for-each-ref',
3691 '--format=%(refname:short) %(upstream:short)',
3692 'refs/heads'])
3693 if not branches:
3694 print('No local branches found.')
3695 return 0
3696
3697 # Create a dictionary of all local branches to the branches that are dependent
3698 # on it.
3699 tracked_to_dependents = collections.defaultdict(list)
3700 for b in branches.splitlines():
3701 tokens = b.split()
3702 if len(tokens) == 2:
3703 branch_name, tracked = tokens
3704 tracked_to_dependents[tracked].append(branch_name)
3705
vapiera7fbd5a2016-06-16 09:17:49 -07003706 print()
3707 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003708 dependents = []
3709 def traverse_dependents_preorder(branch, padding=''):
3710 dependents_to_process = tracked_to_dependents.get(branch, [])
3711 padding += ' '
3712 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003713 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003714 dependents.append(dependent)
3715 traverse_dependents_preorder(dependent, padding)
3716 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003717 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003718
3719 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003720 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003721 return 0
3722
vapiera7fbd5a2016-06-16 09:17:49 -07003723 print('This command will checkout all dependent branches and run '
3724 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003725 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3726
andybons@chromium.org962f9462016-02-03 20:00:42 +00003727 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003728 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003729 args.extend(['-t', 'Updated patchset dependency'])
3730
rmistry@google.com2dd99862015-06-22 12:22:18 +00003731 # Record all dependents that failed to upload.
3732 failures = {}
3733 # Go through all dependents, checkout the branch and upload.
3734 try:
3735 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003736 print()
3737 print('--------------------------------------')
3738 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003739 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003740 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003741 try:
3742 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003743 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003744 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003745 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003746 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003747 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003748 finally:
3749 # Swap back to the original root branch.
3750 RunGit(['checkout', '-q', root_branch])
3751
vapiera7fbd5a2016-06-16 09:17:49 -07003752 print()
3753 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003754 for dependent_branch in dependents:
3755 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003756 print(' %s : %s' % (dependent_branch, upload_status))
3757 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003758
3759 return 0
3760
3761
kmarshall3bff56b2016-06-06 18:31:47 -07003762def CMDarchive(parser, args):
3763 """Archives and deletes branches associated with closed changelists."""
3764 parser.add_option(
3765 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003766 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003767 parser.add_option(
3768 '-f', '--force', action='store_true',
3769 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003770 parser.add_option(
3771 '-d', '--dry-run', action='store_true',
3772 help='Skip the branch tagging and removal steps.')
3773 parser.add_option(
3774 '-t', '--notags', action='store_true',
3775 help='Do not tag archived branches. '
3776 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003777
3778 auth.add_auth_options(parser)
3779 options, args = parser.parse_args(args)
3780 if args:
3781 parser.error('Unsupported args: %s' % ' '.join(args))
3782 auth_config = auth.extract_auth_config_from_options(options)
3783
3784 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3785 if not branches:
3786 return 0
3787
vapiera7fbd5a2016-06-16 09:17:49 -07003788 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003789 changes = [Changelist(branchref=b, auth_config=auth_config)
3790 for b in branches.splitlines()]
3791 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3792 statuses = get_cl_statuses(changes,
3793 fine_grained=True,
3794 max_processes=options.maxjobs)
3795 proposal = [(cl.GetBranch(),
3796 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3797 for cl, status in statuses
3798 if status == 'closed']
3799 proposal.sort()
3800
3801 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003802 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003803 return 0
3804
3805 current_branch = GetCurrentBranch()
3806
vapiera7fbd5a2016-06-16 09:17:49 -07003807 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003808 if options.notags:
3809 for next_item in proposal:
3810 print(' ' + next_item[0])
3811 else:
3812 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3813 for next_item in proposal:
3814 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003815
kmarshall9249e012016-08-23 12:02:16 -07003816 # Quit now on precondition failure or if instructed by the user, either
3817 # via an interactive prompt or by command line flags.
3818 if options.dry_run:
3819 print('\nNo changes were made (dry run).\n')
3820 return 0
3821 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003822 print('You are currently on a branch \'%s\' which is associated with a '
3823 'closed codereview issue, so archive cannot proceed. Please '
3824 'checkout another branch and run this command again.' %
3825 current_branch)
3826 return 1
kmarshall9249e012016-08-23 12:02:16 -07003827 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003828 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3829 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003830 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003831 return 1
3832
3833 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003834 if not options.notags:
3835 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003836 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003837
vapiera7fbd5a2016-06-16 09:17:49 -07003838 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003839
3840 return 0
3841
3842
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003843def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003844 """Show status of changelists.
3845
3846 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003847 - Red not sent for review or broken
3848 - Blue waiting for review
3849 - Yellow waiting for you to reply to review
3850 - Green LGTM'ed
3851 - Magenta in the commit queue
3852 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003853
3854 Also see 'git cl comments'.
3855 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003856 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003857 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003858 parser.add_option('-f', '--fast', action='store_true',
3859 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003860 parser.add_option(
3861 '-j', '--maxjobs', action='store', type=int,
3862 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003863
3864 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003865 _add_codereview_issue_select_options(
3866 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003867 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003868 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003869 if args:
3870 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003871 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003872
iannuccie53c9352016-08-17 14:40:40 -07003873 if options.issue is not None and not options.field:
3874 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003875
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003876 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003877 cl = Changelist(auth_config=auth_config, issue=options.issue,
3878 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003879 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003880 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003881 elif options.field == 'id':
3882 issueid = cl.GetIssue()
3883 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003884 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003885 elif options.field == 'patch':
3886 patchset = cl.GetPatchset()
3887 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003888 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003889 elif options.field == 'status':
3890 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003891 elif options.field == 'url':
3892 url = cl.GetIssueURL()
3893 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003894 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003895 return 0
3896
3897 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3898 if not branches:
3899 print('No local branch found.')
3900 return 0
3901
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003902 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003903 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003904 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003905 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003906 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003907 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003908 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003909
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003910 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003911 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3912 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3913 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003914 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003915 c, status = output.next()
3916 branch_statuses[c.GetBranch()] = status
3917 status = branch_statuses.pop(branch)
3918 url = cl.GetIssueURL()
3919 if url and (not status or status == 'error'):
3920 # The issue probably doesn't exist anymore.
3921 url += ' (broken)'
3922
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003923 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003924 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003925 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003926 color = ''
3927 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003928 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003929 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003930 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003931 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003932
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003933 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003934 print()
3935 print('Current branch:',)
3936 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003937 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003938 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003939 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003940 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003941 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003942 print('Issue description:')
3943 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003944 return 0
3945
3946
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003947def colorize_CMDstatus_doc():
3948 """To be called once in main() to add colors to git cl status help."""
3949 colors = [i for i in dir(Fore) if i[0].isupper()]
3950
3951 def colorize_line(line):
3952 for color in colors:
3953 if color in line.upper():
3954 # Extract whitespaces first and the leading '-'.
3955 indent = len(line) - len(line.lstrip(' ')) + 1
3956 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3957 return line
3958
3959 lines = CMDstatus.__doc__.splitlines()
3960 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3961
3962
phajdan.jre328cf92016-08-22 04:12:17 -07003963def write_json(path, contents):
3964 with open(path, 'w') as f:
3965 json.dump(contents, f)
3966
3967
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003968@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003969def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003970 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003971
3972 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003973 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003974 parser.add_option('-r', '--reverse', action='store_true',
3975 help='Lookup the branch(es) for the specified issues. If '
3976 'no issues are specified, all branches with mapped '
3977 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003978 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003979 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003980 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003981 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003982
dnj@chromium.org406c4402015-03-03 17:22:28 +00003983 if options.reverse:
3984 branches = RunGit(['for-each-ref', 'refs/heads',
3985 '--format=%(refname:short)']).splitlines()
3986
3987 # Reverse issue lookup.
3988 issue_branch_map = {}
3989 for branch in branches:
3990 cl = Changelist(branchref=branch)
3991 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3992 if not args:
3993 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003994 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003995 for issue in args:
3996 if not issue:
3997 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003998 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003999 print('Branch for issue number %s: %s' % (
4000 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004001 if options.json:
4002 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004003 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004004 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004005 if len(args) > 0:
4006 try:
4007 issue = int(args[0])
4008 except ValueError:
4009 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00004010 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00004011 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07004012 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07004013 if options.json:
4014 write_json(options.json, {
4015 'issue': cl.GetIssue(),
4016 'issue_url': cl.GetIssueURL(),
4017 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004018 return 0
4019
4020
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004021def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004022 """Shows or posts review comments for any changelist."""
4023 parser.add_option('-a', '--add-comment', dest='comment',
4024 help='comment to add to an issue')
4025 parser.add_option('-i', dest='issue',
4026 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00004027 parser.add_option('-j', '--json-file',
4028 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004029 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004030 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004031 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004032
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004033 issue = None
4034 if options.issue:
4035 try:
4036 issue = int(options.issue)
4037 except ValueError:
4038 DieWithError('A review issue id is expected to be a number')
4039
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00004040 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004041
4042 if options.comment:
4043 cl.AddComment(options.comment)
4044 return 0
4045
4046 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00004047 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00004048 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00004049 summary.append({
4050 'date': message['date'],
4051 'lgtm': False,
4052 'message': message['text'],
4053 'not_lgtm': False,
4054 'sender': message['sender'],
4055 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004056 if message['disapproval']:
4057 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00004058 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004059 elif message['approval']:
4060 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00004061 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004062 elif message['sender'] == data['owner_email']:
4063 color = Fore.MAGENTA
4064 else:
4065 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07004066 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004067 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07004068 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004069 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004070 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00004071 if options.json_file:
4072 with open(options.json_file, 'wb') as f:
4073 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004074 return 0
4075
4076
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004077@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004078def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004079 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004080 parser.add_option('-d', '--display', action='store_true',
4081 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004082 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004083 help='New description to set for this issue (- for stdin, '
4084 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004085 parser.add_option('-f', '--force', action='store_true',
4086 help='Delete any unpublished Gerrit edits for this issue '
4087 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004088
4089 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004090 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004091 options, args = parser.parse_args(args)
4092 _process_codereview_select_options(parser, options)
4093
4094 target_issue = None
4095 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07004096 target_issue = ParseIssueNumberArgument(args[0])
4097 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004098 parser.print_help()
4099 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004100
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004101 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004102
martiniss6eda05f2016-06-30 10:18:35 -07004103 kwargs = {
4104 'auth_config': auth_config,
4105 'codereview': options.forced_codereview,
4106 }
4107 if target_issue:
4108 kwargs['issue'] = target_issue.issue
4109 if options.forced_codereview == 'rietveld':
4110 kwargs['rietveld_server'] = target_issue.hostname
4111
4112 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004113
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004114 if not cl.GetIssue():
4115 DieWithError('This branch has no associated changelist.')
4116 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004117
smut@google.com34fb6b12015-07-13 20:03:26 +00004118 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004119 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004120 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004121
4122 if options.new_description:
4123 text = options.new_description
4124 if text == '-':
4125 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004126 elif text == '+':
4127 base_branch = cl.GetCommonAncestorWithUpstream()
4128 change = cl.GetChange(base_branch, None, local_description=True)
4129 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004130
4131 description.set_description(text)
4132 else:
4133 description.prompt()
4134
wychen@chromium.org063e4e52015-04-03 06:51:44 +00004135 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004136 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004137 return 0
4138
4139
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004140def CreateDescriptionFromLog(args):
4141 """Pulls out the commit log to use as a base for the CL description."""
4142 log_args = []
4143 if len(args) == 1 and not args[0].endswith('.'):
4144 log_args = [args[0] + '..']
4145 elif len(args) == 1 and args[0].endswith('...'):
4146 log_args = [args[0][:-1]]
4147 elif len(args) == 2:
4148 log_args = [args[0] + '..' + args[1]]
4149 else:
4150 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004151 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004152
4153
thestig@chromium.org44202a22014-03-11 19:22:18 +00004154def CMDlint(parser, args):
4155 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004156 parser.add_option('--filter', action='append', metavar='-x,+y',
4157 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004158 auth.add_auth_options(parser)
4159 options, args = parser.parse_args(args)
4160 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004161
4162 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004163 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004164 try:
4165 import cpplint
4166 import cpplint_chromium
4167 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004168 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004169 return 1
4170
4171 # Change the current working directory before calling lint so that it
4172 # shows the correct base.
4173 previous_cwd = os.getcwd()
4174 os.chdir(settings.GetRoot())
4175 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004176 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004177 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4178 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004179 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004180 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004181 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004182
4183 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004184 command = args + files
4185 if options.filter:
4186 command = ['--filter=' + ','.join(options.filter)] + command
4187 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004188
4189 white_regex = re.compile(settings.GetLintRegex())
4190 black_regex = re.compile(settings.GetLintIgnoreRegex())
4191 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4192 for filename in filenames:
4193 if white_regex.match(filename):
4194 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004195 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004196 else:
4197 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4198 extra_check_functions)
4199 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004200 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004201 finally:
4202 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004203 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004204 if cpplint._cpplint_state.error_count != 0:
4205 return 1
4206 return 0
4207
4208
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004209def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004210 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004211 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004212 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004213 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004214 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004215 auth.add_auth_options(parser)
4216 options, args = parser.parse_args(args)
4217 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004218
sbc@chromium.org71437c02015-04-09 19:29:40 +00004219 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004220 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004221 return 1
4222
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004223 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004224 if args:
4225 base_branch = args[0]
4226 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004227 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004228 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004229
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004230 cl.RunHook(
4231 committing=not options.upload,
4232 may_prompt=False,
4233 verbose=options.verbose,
4234 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004235 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004236
4237
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004238def GenerateGerritChangeId(message):
4239 """Returns Ixxxxxx...xxx change id.
4240
4241 Works the same way as
4242 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4243 but can be called on demand on all platforms.
4244
4245 The basic idea is to generate git hash of a state of the tree, original commit
4246 message, author/committer info and timestamps.
4247 """
4248 lines = []
4249 tree_hash = RunGitSilent(['write-tree'])
4250 lines.append('tree %s' % tree_hash.strip())
4251 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4252 if code == 0:
4253 lines.append('parent %s' % parent.strip())
4254 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4255 lines.append('author %s' % author.strip())
4256 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4257 lines.append('committer %s' % committer.strip())
4258 lines.append('')
4259 # Note: Gerrit's commit-hook actually cleans message of some lines and
4260 # whitespace. This code is not doing this, but it clearly won't decrease
4261 # entropy.
4262 lines.append(message)
4263 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4264 stdin='\n'.join(lines))
4265 return 'I%s' % change_hash.strip()
4266
4267
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004268def GetTargetRef(remote, remote_branch, target_branch, pending_prefix_check,
4269 remote_url=None):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004270 """Computes the remote branch ref to use for the CL.
4271
4272 Args:
4273 remote (str): The git remote for the CL.
4274 remote_branch (str): The git remote branch for the CL.
4275 target_branch (str): The target branch specified by the user.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004276 pending_prefix_check (bool): If true, determines if pending_prefix should be
4277 used.
4278 remote_url (str): Only used for checking if pending_prefix should be used.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004279 """
4280 if not (remote and remote_branch):
4281 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004282
wittman@chromium.org455dc922015-01-26 20:15:50 +00004283 if target_branch:
4284 # Cannonicalize branch references to the equivalent local full symbolic
4285 # refs, which are then translated into the remote full symbolic refs
4286 # below.
4287 if '/' not in target_branch:
4288 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4289 else:
4290 prefix_replacements = (
4291 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4292 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4293 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4294 )
4295 match = None
4296 for regex, replacement in prefix_replacements:
4297 match = re.search(regex, target_branch)
4298 if match:
4299 remote_branch = target_branch.replace(match.group(0), replacement)
4300 break
4301 if not match:
4302 # This is a branch path but not one we recognize; use as-is.
4303 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004304 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4305 # Handle the refs that need to land in different refs.
4306 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004307
wittman@chromium.org455dc922015-01-26 20:15:50 +00004308 # Create the true path to the remote branch.
4309 # Does the following translation:
4310 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4311 # * refs/remotes/origin/master -> refs/heads/master
4312 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4313 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4314 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4315 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4316 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4317 'refs/heads/')
4318 elif remote_branch.startswith('refs/remotes/branch-heads'):
4319 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004320
4321 if pending_prefix_check:
4322 # If a pending prefix exists then replace refs/ with it.
4323 state = _GitNumbererState.load(remote_url, remote_branch)
4324 if state.pending_prefix:
4325 remote_branch = remote_branch.replace('refs/', state.pending_prefix)
wittman@chromium.org455dc922015-01-26 20:15:50 +00004326 return remote_branch
4327
4328
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004329def cleanup_list(l):
4330 """Fixes a list so that comma separated items are put as individual items.
4331
4332 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4333 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4334 """
4335 items = sum((i.split(',') for i in l), [])
4336 stripped_items = (i.strip() for i in items)
4337 return sorted(filter(None, stripped_items))
4338
4339
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004340@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004341def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004342 """Uploads the current changelist to codereview.
4343
4344 Can skip dependency patchset uploads for a branch by running:
4345 git config branch.branch_name.skip-deps-uploads True
4346 To unset run:
4347 git config --unset branch.branch_name.skip-deps-uploads
4348 Can also set the above globally by using the --global flag.
4349 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004350 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4351 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004352 parser.add_option('--bypass-watchlists', action='store_true',
4353 dest='bypass_watchlists',
4354 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004355 parser.add_option('-f', action='store_true', dest='force',
4356 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00004357 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004358 parser.add_option('-b', '--bug',
4359 help='pre-populate the bug number(s) for this issue. '
4360 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004361 parser.add_option('--message-file', dest='message_file',
4362 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00004363 parser.add_option('-t', dest='title',
4364 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004365 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004366 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004367 help='reviewer email addresses')
4368 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004369 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004370 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004371 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004372 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004373 parser.add_option('--emulate_svn_auto_props',
4374 '--emulate-svn-auto-props',
4375 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004376 dest="emulate_svn_auto_props",
4377 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004378 parser.add_option('-c', '--use-commit-queue', action='store_true',
4379 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004380 parser.add_option('--private', action='store_true',
4381 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004382 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004383 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004384 metavar='TARGET',
4385 help='Apply CL to remote ref TARGET. ' +
4386 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004387 parser.add_option('--squash', action='store_true',
4388 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004389 parser.add_option('--no-squash', action='store_true',
4390 help='Don\'t squash multiple commits into one ' +
4391 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004392 parser.add_option('--topic', default=None,
4393 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004394 parser.add_option('--email', default=None,
4395 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004396 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4397 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004398 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4399 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004400 help='Send the patchset to do a CQ dry run right after '
4401 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004402 parser.add_option('--dependencies', action='store_true',
4403 help='Uploads CLs of all the local branches that depend on '
4404 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004405
rmistry@google.com2dd99862015-06-22 12:22:18 +00004406 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004407 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004408 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004409 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004410 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004411 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004412 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004413
sbc@chromium.org71437c02015-04-09 19:29:40 +00004414 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004415 return 1
4416
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004417 options.reviewers = cleanup_list(options.reviewers)
4418 options.cc = cleanup_list(options.cc)
4419
tandriib80458a2016-06-23 12:20:07 -07004420 if options.message_file:
4421 if options.message:
4422 parser.error('only one of --message and --message-file allowed.')
4423 options.message = gclient_utils.FileRead(options.message_file)
4424 options.message_file = None
4425
tandrii4d0545a2016-07-06 03:56:49 -07004426 if options.cq_dry_run and options.use_commit_queue:
4427 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4428
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004429 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4430 settings.GetIsGerrit()
4431
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004432 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004433 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004434
4435
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004436def IsSubmoduleMergeCommit(ref):
4437 # When submodules are added to the repo, we expect there to be a single
4438 # non-git-svn merge commit at remote HEAD with a signature comment.
4439 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00004440 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004441 return RunGit(cmd) != ''
4442
4443
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004444def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004445 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004446
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004447 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4448 upstream and closes the issue automatically and atomically.
4449
4450 Otherwise (in case of Rietveld):
4451 Squashes branch into a single commit.
Andrii Shyshkalov06a25022016-11-24 16:47:00 +01004452 Updates commit message with metadata (e.g. pointer to review).
4453 Pushes the code upstream.
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004454 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004455 """
4456 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4457 help='bypass upload presubmit hook')
4458 parser.add_option('-m', dest='message',
4459 help="override review description")
4460 parser.add_option('-f', action='store_true', dest='force',
4461 help="force yes to questions (don't prompt)")
4462 parser.add_option('-c', dest='contributor',
4463 help="external contributor for patch (appended to " +
4464 "description and used as author for git). Should be " +
4465 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004466 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004467 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004468 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004469 auth_config = auth.extract_auth_config_from_options(options)
4470
4471 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004472
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004473 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4474 if cl.IsGerrit():
4475 if options.message:
4476 # This could be implemented, but it requires sending a new patch to
4477 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4478 # Besides, Gerrit has the ability to change the commit message on submit
4479 # automatically, thus there is no need to support this option (so far?).
4480 parser.error('-m MESSAGE option is not supported for Gerrit.')
4481 if options.contributor:
4482 parser.error(
4483 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4484 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4485 'the contributor\'s "name <email>". If you can\'t upload such a '
4486 'commit for review, contact your repository admin and request'
4487 '"Forge-Author" permission.')
tandrii73449b02016-09-14 06:27:24 -07004488 if not cl.GetIssue():
Aaron Gablea45ee112016-11-22 15:14:38 -08004489 DieWithError('You must upload the change first to Gerrit.\n'
tandrii73449b02016-09-14 06:27:24 -07004490 ' If you would rather have `git cl land` upload '
4491 'automatically for you, see http://crbug.com/642759')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004492 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4493 options.verbose)
4494
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004495 current = cl.GetBranch()
4496 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4497 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07004498 print()
4499 print('Attempting to push branch %r into another local branch!' % current)
4500 print()
4501 print('Either reparent this branch on top of origin/master:')
4502 print(' git reparent-branch --root')
4503 print()
4504 print('OR run `git rebase-update` if you think the parent branch is ')
4505 print('already committed.')
4506 print()
4507 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004508 return 1
4509
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004510 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004511 # Default to merging against our best guess of the upstream branch.
4512 args = [cl.GetUpstreamBranch()]
4513
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004514 if options.contributor:
4515 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07004516 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004517 return 1
4518
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004519 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004520 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004521
sbc@chromium.org71437c02015-04-09 19:29:40 +00004522 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004523 return 1
4524
4525 # This rev-list syntax means "show all commits not in my branch that
4526 # are in base_branch".
4527 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4528 base_branch]).splitlines()
4529 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004530 print('Base branch "%s" has %d commits '
4531 'not in this branch.' % (base_branch, len(upstream_commits)))
4532 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004533 return 1
4534
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004535 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004536 svn_head = None
4537 if cmd == 'dcommit' or base_has_submodules:
4538 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
4539 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004540
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004541 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004542 # If the base_head is a submodule merge commit, the first parent of the
4543 # base_head should be a git-svn commit, which is what we're interested in.
4544 base_svn_head = base_branch
4545 if base_has_submodules:
4546 base_svn_head += '^1'
4547
4548 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004549 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004550 print('This branch has %d additional commits not upstreamed yet.'
4551 % len(extra_commits.splitlines()))
4552 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
4553 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004554 return 1
4555
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004556 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004557 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004558 author = None
4559 if options.contributor:
4560 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004561 hook_results = cl.RunHook(
4562 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004563 may_prompt=not options.force,
4564 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004565 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004566 if not hook_results.should_continue():
4567 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004568
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004569 # Check the tree status if the tree status URL is set.
4570 status = GetTreeStatus()
4571 if 'closed' == status:
4572 print('The tree is closed. Please wait for it to reopen. Use '
4573 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4574 return 1
4575 elif 'unknown' == status:
4576 print('Unable to determine tree status. Please verify manually and '
4577 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4578 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004579
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004580 change_desc = ChangeDescription(options.message)
4581 if not change_desc.description and cl.GetIssue():
4582 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004583
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004584 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00004585 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004586 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00004587 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004588 print('No description set.')
4589 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00004590 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004591
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004592 # Keep a separate copy for the commit message, because the commit message
4593 # contains the link to the Rietveld issue, while the Rietveld message contains
4594 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004595 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00004596 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004597
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004598 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00004599 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00004600 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00004601 # after it. Add a period on a new line to circumvent this. Also add a space
4602 # before the period to make sure that Gitiles continues to correctly resolve
4603 # the URL.
Andrii Shyshkalovf01be222016-12-08 19:21:13 +01004604 commit_desc.append_footer('Review-Url: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004605 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004606 commit_desc.append_footer('Patch from %s.' % options.contributor)
4607
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00004608 print('Description:')
4609 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004610
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004611 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004612 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00004613 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004614
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004615 # We want to squash all this branch's commits into one commit with the proper
4616 # description. We do this by doing a "reset --soft" to the base branch (which
4617 # keeps the working copy the same), then dcommitting that. If origin/master
4618 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4619 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004620 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004621 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4622 # Delete the branches if they exist.
4623 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4624 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4625 result = RunGitWithCode(showref_cmd)
4626 if result[0] == 0:
4627 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004628
4629 # We might be in a directory that's present in this branch but not in the
4630 # trunk. Move up to the top of the tree so that git commands that expect a
4631 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004632 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004633 if rel_base_path:
4634 os.chdir(rel_base_path)
4635
4636 # Stuff our change into the merge branch.
4637 # We wrap in a try...finally block so if anything goes wrong,
4638 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004639 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004640 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004641 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004642 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004643 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004644 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004645 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004646 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004647 RunGit(
4648 [
4649 'commit', '--author', options.contributor,
4650 '-m', commit_desc.description,
4651 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004652 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004653 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004654 if base_has_submodules:
4655 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4656 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4657 RunGit(['checkout', CHERRY_PICK_BRANCH])
4658 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004659 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004660 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004661 logging.debug('remote: %s, branch %s', remote, branch)
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004662 mirror = settings.GetGitMirror(remote)
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004663 if mirror:
4664 pushurl = mirror.url
4665 git_numberer = _GitNumbererState.load(pushurl, branch)
4666 else:
4667 pushurl = remote # Usually, this is 'origin'.
4668 git_numberer = _GitNumbererState.load(
4669 RunGit(['config', 'remote.%s.url' % remote]).strip(), branch)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01004670
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004671 pending_prefix = git_numberer.pending_prefix
4672
Andrii Shyshkalov8f15f3e2016-12-14 15:43:49 +01004673 if git_numberer.should_add_git_number:
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01004674 # TODO(tandrii): run git fetch in a loop + autorebase when there there
4675 # is no pending ref to push to?
4676 logging.debug('Adding git number footers')
4677 parent_msg = RunGit(['show', '-s', '--format=%B', merge_base]).strip()
4678 commit_desc.update_with_git_number_footers(merge_base, parent_msg,
4679 branch)
Andrii Shyshkalova6695812016-12-06 17:47:09 +01004680 # Ensure timestamps are monotonically increasing.
4681 timestamp = max(1 + _get_committer_timestamp(merge_base),
4682 _get_committer_timestamp('HEAD'))
4683 _git_amend_head(commit_desc.description, timestamp)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01004684 change_desc = ChangeDescription(commit_desc.description)
4685 # If gnumbd is sitll ON and we ultimately push to branch with
4686 # pending_prefix, gnumbd will modify footers we've just inserted with
4687 # 'Original-', which is annoying but still technically correct.
4688
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004689 if not pending_prefix or branch.startswith(pending_prefix):
4690 # If not using refs/pending/heads/* at all, or target ref is already set
4691 # to pending, then push to the target ref directly.
Andrii Shyshkalov813ec3c2016-11-24 17:06:01 +01004692 # NB(tandrii): I think branch.startswith(pending_prefix) never happens
4693 # in practise. I really tried to create a new branch tracking
4694 # refs/pending/heads/master directly and git cl land failed long before
4695 # reaching this. Disagree? Comment on http://crbug.com/642493.
4696 if pending_prefix:
4697 print('\n\nYOU GOT A CHANCE TO WIN A FREE GIFT!\n\n'
4698 'Grab your .git/config, add instructions how to reproduce '
4699 'this, and post it to http://crbug.com/642493.\n'
4700 'The first reporter gets a free "Black Swan" book from '
4701 'tandrii@\n\n')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004702 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004703 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004704 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004705 else:
4706 # Cherry-pick the change on top of pending ref and then push it.
4707 assert branch.startswith('refs/'), branch
4708 assert pending_prefix[-1] == '/', pending_prefix
4709 pending_ref = pending_prefix + branch[len('refs/'):]
tandriibf429402016-09-14 07:09:12 -07004710 retcode, output = PushToGitPending(pushurl, pending_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004711 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004712 if retcode == 0:
4713 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004714 else:
4715 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004716 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004717 'svn', 'dcommit',
4718 '-C%s' % options.similarity,
4719 '--no-rebase', '--rmdir',
4720 ]
4721 if settings.GetForceHttpsCommitUrl():
4722 # Allow forcing https commit URLs for some projects that don't allow
4723 # committing to http URLs (like Google Code).
4724 remote_url = cl.GetGitSvnRemoteUrl()
4725 if urlparse.urlparse(remote_url).scheme == 'http':
4726 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004727 cmd_args.append('--commit-url=%s' % remote_url)
4728 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004729 if 'Committed r' in output:
4730 revision = re.match(
4731 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4732 logging.debug(output)
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004733 except: # pylint: disable=bare-except
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004734 if _IS_BEING_TESTED:
4735 logging.exception('this is likely your ACTUAL cause of test failure.\n'
4736 + '-' * 30 + '8<' + '-' * 30)
4737 logging.error('\n' + '-' * 30 + '8<' + '-' * 30 + '\n\n\n')
4738 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004739 finally:
4740 # And then swap back to the original branch and clean up.
4741 RunGit(['checkout', '-q', cl.GetBranch()])
4742 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004743 if base_has_submodules:
4744 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004745
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004746 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004747 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004748 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004749
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004750 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004751 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004752 try:
4753 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4754 # We set pushed_to_pending to False, since it made it all the way to the
4755 # real ref.
4756 pushed_to_pending = False
4757 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004758 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004759
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004760 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004761 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004762 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004763 if not to_pending:
4764 if viewvc_url and revision:
4765 change_desc.append_footer(
4766 'Committed: %s%s' % (viewvc_url, revision))
4767 elif revision:
4768 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004769 print('Closing issue '
4770 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004771 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004772 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004773 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004774 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004775 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004776 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004777 if options.bypass_hooks:
4778 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4779 else:
4780 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004781 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004782
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004783 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004784 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004785 print('The commit is in the pending queue (%s).' % pending_ref)
4786 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4787 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004788
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004789 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4790 if os.path.isfile(hook):
4791 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004792
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004793 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004794
4795
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004796def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004797 print()
4798 print('Waiting for commit to be landed on %s...' % real_ref)
4799 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004800 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4801 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004802 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004803
4804 loop = 0
4805 while True:
4806 sys.stdout.write('fetching (%d)... \r' % loop)
4807 sys.stdout.flush()
4808 loop += 1
4809
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004810 if mirror:
4811 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004812 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4813 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4814 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4815 for commit in commits.splitlines():
4816 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004817 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004818 return commit
4819
4820 current_rev = to_rev
4821
4822
tandriibf429402016-09-14 07:09:12 -07004823def PushToGitPending(remote, pending_ref):
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004824 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4825
4826 Returns:
4827 (retcode of last operation, output log of last operation).
4828 """
4829 assert pending_ref.startswith('refs/'), pending_ref
4830 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4831 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4832 code = 0
4833 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004834 max_attempts = 3
4835 attempts_left = max_attempts
4836 while attempts_left:
4837 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004838 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004839 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004840
4841 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004842 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004843 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004844 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004845 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004846 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004847 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004848 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004849 continue
4850
4851 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004852 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004853 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004854 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004855 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004856 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4857 'the following files have merge conflicts:' % pending_ref)
4858 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4859 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004860 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004861 return code, out
4862
4863 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004864 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004865 code, out = RunGitWithCode(
4866 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4867 if code == 0:
4868 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004869 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004870 return code, out
4871
vapiera7fbd5a2016-06-16 09:17:49 -07004872 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004873 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004874 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004875 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004876 print('Fatal push error. Make sure your .netrc credentials and git '
4877 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004878 return code, out
4879
vapiera7fbd5a2016-06-16 09:17:49 -07004880 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004881 return code, out
4882
4883
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004884def IsFatalPushFailure(push_stdout):
4885 """True if retrying push won't help."""
4886 return '(prohibited by Gerrit)' in push_stdout
4887
4888
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004889@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004890def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004891 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004892 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004893 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004894 # If it looks like previous commits were mirrored with git-svn.
agable3b9a5bb2016-09-22 11:32:08 -07004895 message = """This repository appears to be a git-svn mirror, but we
4896don't support git-svn mirrors anymore."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004897 else:
4898 message = """This doesn't appear to be an SVN repository.
4899If your project has a true, writeable git repository, you probably want to run
4900'git cl land' instead.
4901If your project has a git mirror of an upstream SVN master, you probably need
4902to run 'git svn init'.
4903
4904Using the wrong command might cause your commit to appear to succeed, and the
4905review to be closed, without actually landing upstream. If you choose to
4906proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004907 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004908 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004909 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4910 'Please let us know of this project you are committing to:'
4911 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004912 return SendUpstream(parser, args, 'dcommit')
4913
4914
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004915@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004916def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004917 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004918 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004919 print('This appears to be an SVN repository.')
4920 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004921 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004922 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004923 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004924
4925
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004926@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004927def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004928 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004929 parser.add_option('-b', dest='newbranch',
4930 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004931 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004932 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004933 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4934 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004935 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004936 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004937 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004938 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004939 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004940 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004941
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004942
4943 group = optparse.OptionGroup(
4944 parser,
4945 'Options for continuing work on the current issue uploaded from a '
4946 'different clone (e.g. different machine). Must be used independently '
4947 'from the other options. No issue number should be specified, and the '
4948 'branch must have an issue number associated with it')
4949 group.add_option('--reapply', action='store_true', dest='reapply',
4950 help='Reset the branch and reapply the issue.\n'
4951 'CAUTION: This will undo any local changes in this '
4952 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004953
4954 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004955 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004956 parser.add_option_group(group)
4957
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004958 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004959 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004960 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004961 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004962 auth_config = auth.extract_auth_config_from_options(options)
4963
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004964
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004965 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004966 if options.newbranch:
4967 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004968 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004969 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004970
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004971 cl = Changelist(auth_config=auth_config,
4972 codereview=options.forced_codereview)
4973 if not cl.GetIssue():
4974 parser.error('current branch must have an associated issue')
4975
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004976 upstream = cl.GetUpstreamBranch()
4977 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004978 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004979
4980 RunGit(['reset', '--hard', upstream])
4981 if options.pull:
4982 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004983
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004984 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4985 options.directory)
4986
4987 if len(args) != 1 or not args[0]:
4988 parser.error('Must specify issue number or url')
4989
4990 # We don't want uncommitted changes mixed up with the patch.
4991 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004992 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004993
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004994 if options.newbranch:
4995 if options.force:
4996 RunGit(['branch', '-D', options.newbranch],
4997 stderr=subprocess2.PIPE, error_ok=True)
4998 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004999 elif not GetCurrentBranch():
5000 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005001
5002 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
5003
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005004 if cl.IsGerrit():
5005 if options.reject:
5006 parser.error('--reject is not supported with Gerrit codereview.')
5007 if options.nocommit:
5008 parser.error('--nocommit is not supported with Gerrit codereview.')
5009 if options.directory:
5010 parser.error('--directory is not supported with Gerrit codereview.')
5011
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005012 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005013 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005014
5015
5016def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005017 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005018 # Provide a wrapper for git svn rebase to help avoid accidental
5019 # git svn dcommit.
5020 # It's the only command that doesn't use parser at all since we just defer
5021 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00005022
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005023 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005024
5025
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005026def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005027 """Fetches the tree status and returns either 'open', 'closed',
5028 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005029 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005030 if url:
5031 status = urllib2.urlopen(url).read().lower()
5032 if status.find('closed') != -1 or status == '0':
5033 return 'closed'
5034 elif status.find('open') != -1 or status == '1':
5035 return 'open'
5036 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005037 return 'unset'
5038
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005039
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005040def GetTreeStatusReason():
5041 """Fetches the tree status from a json url and returns the message
5042 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005043 url = settings.GetTreeStatusUrl()
5044 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005045 connection = urllib2.urlopen(json_url)
5046 status = json.loads(connection.read())
5047 connection.close()
5048 return status['message']
5049
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005050
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005051def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005052 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005053 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005054 status = GetTreeStatus()
5055 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005056 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005057 return 2
5058
vapiera7fbd5a2016-06-16 09:17:49 -07005059 print('The tree is %s' % status)
5060 print()
5061 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005062 if status != 'open':
5063 return 1
5064 return 0
5065
5066
maruel@chromium.org15192402012-09-06 12:38:29 +00005067def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005068 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005069 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005070 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005071 '-b', '--bot', action='append',
5072 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5073 'times to specify multiple builders. ex: '
5074 '"-b win_rel -b win_layout". See '
5075 'the try server waterfall for the builders name and the tests '
5076 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005077 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005078 '-B', '--bucket', default='',
5079 help=('Buildbucket bucket to send the try requests.'))
5080 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005081 '-m', '--master', default='',
5082 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005083 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005084 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005085 help='Revision to use for the try job; default: the revision will '
5086 'be determined by the try recipe that builder runs, which usually '
5087 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005088 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005089 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005090 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005091 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005092 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005093 '--project',
5094 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005095 'in recipe to determine to which repository or directory to '
5096 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005097 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005098 '-p', '--property', dest='properties', action='append', default=[],
5099 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005100 'key2=value2 etc. The value will be treated as '
5101 'json if decodable, or as string otherwise. '
5102 'NOTE: using this may make your try job not usable for CQ, '
5103 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005104 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005105 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5106 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005107 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005108 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005109 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005110 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005111
machenbach@chromium.org45453142015-09-15 08:45:22 +00005112 # Make sure that all properties are prop=value pairs.
5113 bad_params = [x for x in options.properties if '=' not in x]
5114 if bad_params:
5115 parser.error('Got properties with missing "=": %s' % bad_params)
5116
maruel@chromium.org15192402012-09-06 12:38:29 +00005117 if args:
5118 parser.error('Unknown arguments: %s' % args)
5119
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005120 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00005121 if not cl.GetIssue():
5122 parser.error('Need to upload first')
5123
tandriie113dfd2016-10-11 10:20:12 -07005124 error_message = cl.CannotTriggerTryJobReason()
5125 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005126 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005127
borenet6c0efe62016-10-19 08:13:29 -07005128 if options.bucket and options.master:
5129 parser.error('Only one of --bucket and --master may be used.')
5130
qyearsley1fdfcb62016-10-24 13:22:03 -07005131 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005132
qyearsleydd49f942016-10-28 11:57:22 -07005133 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5134 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005135 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005136 if options.verbose:
5137 print('git cl try with no bots now defaults to CQ Dry Run.')
5138 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00005139
borenet6c0efe62016-10-19 08:13:29 -07005140 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005141 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005142 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005143 'of bot requires an initial job from a parent (usually a builder). '
5144 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005145 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005146 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005147
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005148 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005149 # TODO(tandrii): Checking local patchset against remote patchset is only
5150 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5151 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07005152 print('Warning: Codereview server has newer patchsets (%s) than most '
5153 'recent upload from local checkout (%s). Did a previous upload '
5154 'fail?\n'
5155 'By default, git cl try uses the latest patchset from '
5156 'codereview, continuing to use patchset %s.\n' %
5157 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07005158
tandrii568043b2016-10-11 07:49:18 -07005159 try:
borenet6c0efe62016-10-19 08:13:29 -07005160 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
5161 patchset)
tandrii568043b2016-10-11 07:49:18 -07005162 except BuildbucketResponseException as ex:
5163 print('ERROR: %s' % ex)
5164 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005165 return 0
5166
5167
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005168def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005169 """Prints info about try jobs associated with current CL."""
5170 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005171 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005172 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005173 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005174 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005175 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005176 '--color', action='store_true', default=setup_color.IS_TTY,
5177 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005178 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005179 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5180 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005181 group.add_option(
5182 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005183 parser.add_option_group(group)
5184 auth.add_auth_options(parser)
5185 options, args = parser.parse_args(args)
5186 if args:
5187 parser.error('Unrecognized args: %s' % ' '.join(args))
5188
5189 auth_config = auth.extract_auth_config_from_options(options)
5190 cl = Changelist(auth_config=auth_config)
5191 if not cl.GetIssue():
5192 parser.error('Need to upload first')
5193
tandrii221ab252016-10-06 08:12:04 -07005194 patchset = options.patchset
5195 if not patchset:
5196 patchset = cl.GetMostRecentPatchset()
5197 if not patchset:
5198 parser.error('Codereview doesn\'t know about issue %s. '
5199 'No access to issue or wrong issue number?\n'
5200 'Either upload first, or pass --patchset explicitely' %
5201 cl.GetIssue())
5202
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005203 # TODO(tandrii): Checking local patchset against remote patchset is only
5204 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5205 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07005206 print('Warning: Codereview server has newer patchsets (%s) than most '
5207 'recent upload from local checkout (%s). Did a previous upload '
5208 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07005209 'By default, git cl try-results uses the latest patchset from '
5210 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07005211 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005212 try:
tandrii221ab252016-10-06 08:12:04 -07005213 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005214 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005215 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005216 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005217 if options.json:
5218 write_try_results_json(options.json, jobs)
5219 else:
5220 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005221 return 0
5222
5223
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005224@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005225def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005226 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005227 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005228 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005229 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005230
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005231 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005232 if args:
5233 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005234 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005235 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005236 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005237 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005238
5239 # Clear configured merge-base, if there is one.
5240 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005241 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005242 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005243 return 0
5244
5245
thestig@chromium.org00858c82013-12-02 23:08:03 +00005246def CMDweb(parser, args):
5247 """Opens the current CL in the web browser."""
5248 _, args = parser.parse_args(args)
5249 if args:
5250 parser.error('Unrecognized args: %s' % ' '.join(args))
5251
5252 issue_url = Changelist().GetIssueURL()
5253 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005254 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005255 return 1
5256
5257 webbrowser.open(issue_url)
5258 return 0
5259
5260
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005261def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005262 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005263 parser.add_option('-d', '--dry-run', action='store_true',
5264 help='trigger in dry run mode')
5265 parser.add_option('-c', '--clear', action='store_true',
5266 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005267 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005268 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005269 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005270 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005271 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005272 if args:
5273 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005274 if options.dry_run and options.clear:
5275 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5276
iannuccie53c9352016-08-17 14:40:40 -07005277 cl = Changelist(auth_config=auth_config, issue=options.issue,
5278 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005279 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005280 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005281 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07005282 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005283 state = _CQState.DRY_RUN
5284 else:
5285 state = _CQState.COMMIT
5286 if not cl.GetIssue():
5287 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005288 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005289 return 0
5290
5291
groby@chromium.org411034a2013-02-26 15:12:01 +00005292def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005293 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005294 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005295 auth.add_auth_options(parser)
5296 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005297 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005298 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005299 if args:
5300 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005301 cl = Changelist(auth_config=auth_config, issue=options.issue,
5302 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005303 # Ensure there actually is an issue to close.
5304 cl.GetDescription()
5305 cl.CloseIssue()
5306 return 0
5307
5308
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005309def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005310 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005311 parser.add_option(
5312 '--stat',
5313 action='store_true',
5314 dest='stat',
5315 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005316 auth.add_auth_options(parser)
5317 options, args = parser.parse_args(args)
5318 auth_config = auth.extract_auth_config_from_options(options)
5319 if args:
5320 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005321
5322 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005323 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005324 # Staged changes would be committed along with the patch from last
5325 # upload, hence counted toward the "last upload" side in the final
5326 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005327 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005328 return 1
5329
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005330 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005331 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005332 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005333 if not issue:
5334 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005335 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005336 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005337
5338 # Create a new branch based on the merge-base
5339 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005340 # Clear cached branch in cl object, to avoid overwriting original CL branch
5341 # properties.
5342 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005343 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005344 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005345 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005346 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005347 return rtn
5348
wychen@chromium.org06928532015-02-03 02:11:29 +00005349 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005350 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005351 cmd = ['git', 'diff']
5352 if options.stat:
5353 cmd.append('--stat')
5354 cmd.extend([TMP_BRANCH, branch, '--'])
5355 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005356 finally:
5357 RunGit(['checkout', '-q', branch])
5358 RunGit(['branch', '-D', TMP_BRANCH])
5359
5360 return 0
5361
5362
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005363def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005364 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005365 parser.add_option(
5366 '--no-color',
5367 action='store_true',
5368 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005369 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005370 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005371 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005372
5373 author = RunGit(['config', 'user.email']).strip() or None
5374
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005375 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005376
5377 if args:
5378 if len(args) > 1:
5379 parser.error('Unknown args')
5380 base_branch = args[0]
5381 else:
5382 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005383 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005384
5385 change = cl.GetChange(base_branch, None)
5386 return owners_finder.OwnersFinder(
5387 [f.LocalPath() for f in
5388 cl.GetChange(base_branch, None).AffectedFiles()],
5389 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005390 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005391 disable_color=options.no_color).run()
5392
5393
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005394def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005395 """Generates a diff command."""
5396 # Generate diff for the current branch's changes.
5397 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
5398 upstream_commit, '--' ]
5399
5400 if args:
5401 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005402 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005403 diff_cmd.append(arg)
5404 else:
5405 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005406
5407 return diff_cmd
5408
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005409def MatchingFileType(file_name, extensions):
5410 """Returns true if the file name ends with one of the given extensions."""
5411 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005412
enne@chromium.org555cfe42014-01-29 18:21:39 +00005413@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005414def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005415 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07005416 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005417 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005418 parser.add_option('--full', action='store_true',
5419 help='Reformat the full content of all touched files')
5420 parser.add_option('--dry-run', action='store_true',
5421 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005422 parser.add_option('--python', action='store_true',
5423 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005424 parser.add_option('--diff', action='store_true',
5425 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005426 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005427
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005428 # git diff generates paths against the root of the repository. Change
5429 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005430 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005431 if rel_base_path:
5432 os.chdir(rel_base_path)
5433
digit@chromium.org29e47272013-05-17 17:01:46 +00005434 # Grab the merge-base commit, i.e. the upstream commit of the current
5435 # branch when it was created or the last time it was rebased. This is
5436 # to cover the case where the user may have called "git fetch origin",
5437 # moving the origin branch to a newer commit, but hasn't rebased yet.
5438 upstream_commit = None
5439 cl = Changelist()
5440 upstream_branch = cl.GetUpstreamBranch()
5441 if upstream_branch:
5442 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5443 upstream_commit = upstream_commit.strip()
5444
5445 if not upstream_commit:
5446 DieWithError('Could not find base commit for this branch. '
5447 'Are you in detached state?')
5448
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005449 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5450 diff_output = RunGit(changed_files_cmd)
5451 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005452 # Filter out files deleted by this CL
5453 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005454
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005455 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5456 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5457 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005458 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005459
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005460 top_dir = os.path.normpath(
5461 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5462
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005463 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5464 # formatted. This is used to block during the presubmit.
5465 return_value = 0
5466
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005467 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005468 # Locate the clang-format binary in the checkout
5469 try:
5470 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005471 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005472 DieWithError(e)
5473
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005474 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005475 cmd = [clang_format_tool]
5476 if not opts.dry_run and not opts.diff:
5477 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005478 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005479 if opts.diff:
5480 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005481 else:
5482 env = os.environ.copy()
5483 env['PATH'] = str(os.path.dirname(clang_format_tool))
5484 try:
5485 script = clang_format.FindClangFormatScriptInChromiumTree(
5486 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005487 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005488 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005489
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005490 cmd = [sys.executable, script, '-p0']
5491 if not opts.dry_run and not opts.diff:
5492 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005493
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005494 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5495 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005496
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005497 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5498 if opts.diff:
5499 sys.stdout.write(stdout)
5500 if opts.dry_run and len(stdout) > 0:
5501 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005502
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005503 # Similar code to above, but using yapf on .py files rather than clang-format
5504 # on C/C++ files
5505 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005506 yapf_tool = gclient_utils.FindExecutable('yapf')
5507 if yapf_tool is None:
5508 DieWithError('yapf not found in PATH')
5509
5510 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005511 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005512 cmd = [yapf_tool]
5513 if not opts.dry_run and not opts.diff:
5514 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005515 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005516 if opts.diff:
5517 sys.stdout.write(stdout)
5518 else:
5519 # TODO(sbc): yapf --lines mode still has some issues.
5520 # https://github.com/google/yapf/issues/154
5521 DieWithError('--python currently only works with --full')
5522
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005523 # Dart's formatter does not have the nice property of only operating on
5524 # modified chunks, so hard code full.
5525 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005526 try:
5527 command = [dart_format.FindDartFmtToolInChromiumTree()]
5528 if not opts.dry_run and not opts.diff:
5529 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005530 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005531
ppi@chromium.org6593d932016-03-03 15:41:15 +00005532 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005533 if opts.dry_run and stdout:
5534 return_value = 2
5535 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005536 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5537 'found in this checkout. Files in other languages are still '
5538 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005539
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005540 # Format GN build files. Always run on full build files for canonical form.
5541 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005542 cmd = ['gn', 'format' ]
5543 if opts.dry_run or opts.diff:
5544 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005545 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005546 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5547 shell=sys.platform == 'win32',
5548 cwd=top_dir)
5549 if opts.dry_run and gn_ret == 2:
5550 return_value = 2 # Not formatted.
5551 elif opts.diff and gn_ret == 2:
5552 # TODO this should compute and print the actual diff.
5553 print("This change has GN build file diff for " + gn_diff_file)
5554 elif gn_ret != 0:
5555 # For non-dry run cases (and non-2 return values for dry-run), a
5556 # nonzero error code indicates a failure, probably because the file
5557 # doesn't parse.
5558 DieWithError("gn format failed on " + gn_diff_file +
5559 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005560
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005561 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005562
5563
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005564@subcommand.usage('<codereview url or issue id>')
5565def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005566 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005567 _, args = parser.parse_args(args)
5568
5569 if len(args) != 1:
5570 parser.print_help()
5571 return 1
5572
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005573 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005574 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005575 parser.print_help()
5576 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005577 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005578
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005579 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005580 output = RunGit(['config', '--local', '--get-regexp',
5581 r'branch\..*\.%s' % issueprefix],
5582 error_ok=True)
5583 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005584 if issue == target_issue:
5585 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005586
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005587 branches = []
5588 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005589 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005590 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005591 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005592 return 1
5593 if len(branches) == 1:
5594 RunGit(['checkout', branches[0]])
5595 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005596 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005597 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005598 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005599 which = raw_input('Choose by index: ')
5600 try:
5601 RunGit(['checkout', branches[int(which)]])
5602 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005603 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005604 return 1
5605
5606 return 0
5607
5608
maruel@chromium.org29404b52014-09-08 22:58:00 +00005609def CMDlol(parser, args):
5610 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005611 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005612 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5613 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5614 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005615 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005616 return 0
5617
5618
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005619class OptionParser(optparse.OptionParser):
5620 """Creates the option parse and add --verbose support."""
5621 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005622 optparse.OptionParser.__init__(
5623 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005624 self.add_option(
5625 '-v', '--verbose', action='count', default=0,
5626 help='Use 2 times for more debugging info')
5627
5628 def parse_args(self, args=None, values=None):
5629 options, args = optparse.OptionParser.parse_args(self, args, values)
5630 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5631 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5632 return options, args
5633
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005634
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005635def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005636 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005637 print('\nYour python version %s is unsupported, please upgrade.\n' %
5638 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005639 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005640
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005641 # Reload settings.
5642 global settings
5643 settings = Settings()
5644
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005645 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005646 dispatcher = subcommand.CommandDispatcher(__name__)
5647 try:
5648 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005649 except auth.AuthenticationError as e:
5650 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005651 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005652 if e.code != 500:
5653 raise
5654 DieWithError(
5655 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5656 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005657 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005658
5659
5660if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005661 # These affect sys.stdout so do it outside of main() to simplify mocks in
5662 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005663 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005664 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005665 try:
5666 sys.exit(main(sys.argv[1:]))
5667 except KeyboardInterrupt:
5668 sys.stderr.write('interrupted\n')
5669 sys.exit(1)