blob: c7a1fc542f6b339340157aecbcaeed8af0844384 [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000012from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000013from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000014import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000015import collections
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +010016import fnmatch
sheyang@google.com6ebaf782015-05-12 19:17:54 +000017import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000018import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000020import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import optparse
22import os
23import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000024import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import sys
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +010026import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000027import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000028import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000029import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000030import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000031import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000032import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000033import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000034import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000035
36try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080037 import readline # pylint: disable=import-error,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000038except ImportError:
39 pass
40
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000041from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000042from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000043from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000044import auth
skobes6468b902016-10-24 08:45:10 -070045import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000046import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000047import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000048import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000049import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000050import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000051import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000052import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000053import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000054import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000055import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000056import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000057import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000059import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000060import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000061import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000062import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000063import watchlists
64
tandrii7400cf02016-06-21 08:48:07 -070065__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000066
tandrii9d2c7a32016-06-22 03:42:45 -070067COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070068DEFAULT_SERVER = 'https://codereview.chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080069POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000070DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +000071REFS_THAT_ALIAS_TO_OTHER_REFS = {
72 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
73 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
74}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000075
thestig@chromium.org44202a22014-03-11 19:22:18 +000076# Valid extensions for files we want to lint.
77DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
78DEFAULT_LINT_IGNORE_REGEX = r"$^"
79
borenet6c0efe62016-10-19 08:13:29 -070080# Buildbucket master name prefix.
81MASTER_PREFIX = 'master.'
82
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000083# Shortcut since it quickly becomes redundant.
84Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000085
maruel@chromium.orgddd59412011-11-30 14:20:38 +000086# Initialized in main()
87settings = None
88
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010089# Used by tests/git_cl_test.py to add extra logging.
90# Inside the weirdly failing test, add this:
91# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
92# And scroll up to see the strack trace printed.
93_IS_BEING_TESTED = False
94
maruel@chromium.orgddd59412011-11-30 14:20:38 +000095
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000096def DieWithError(message):
vapiera7fbd5a2016-06-16 09:17:49 -070097 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000098 sys.exit(1)
99
100
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000101def GetNoGitPagerEnv():
102 env = os.environ.copy()
103 # 'cat' is a magical git string that disables pagers on all platforms.
104 env['GIT_PAGER'] = 'cat'
105 return env
106
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000107
bsep@chromium.org627d9002016-04-29 00:00:52 +0000108def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000109 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000110 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000111 except subprocess2.CalledProcessError as e:
112 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000113 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000114 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000115 'Command "%s" failed.\n%s' % (
116 ' '.join(args), error_message or e.stdout or ''))
117 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000118
119
120def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000121 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000122 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000123
124
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000125def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000126 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700127 if suppress_stderr:
128 stderr = subprocess2.VOID
129 else:
130 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000131 try:
tandrii5d48c322016-08-18 16:19:37 -0700132 (out, _), code = subprocess2.communicate(['git'] + args,
133 env=GetNoGitPagerEnv(),
134 stdout=subprocess2.PIPE,
135 stderr=stderr)
136 return code, out
137 except subprocess2.CalledProcessError as e:
138 logging.debug('Failed running %s', args)
139 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000140
141
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000142def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000143 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000144 return RunGitWithCode(args, suppress_stderr=True)[1]
145
146
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000147def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000148 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000149 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000150 return (version.startswith(prefix) and
151 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000152
153
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000154def BranchExists(branch):
155 """Return True if specified branch exists."""
156 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
157 suppress_stderr=True)
158 return not code
159
160
tandrii2a16b952016-10-19 07:09:44 -0700161def time_sleep(seconds):
162 # Use this so that it can be mocked in tests without interfering with python
163 # system machinery.
164 import time # Local import to discourage others from importing time globally.
165 return time.sleep(seconds)
166
167
maruel@chromium.org90541732011-04-01 17:54:18 +0000168def ask_for_data(prompt):
169 try:
170 return raw_input(prompt)
171 except KeyboardInterrupt:
172 # Hide the exception.
173 sys.exit(1)
174
175
tandrii5d48c322016-08-18 16:19:37 -0700176def _git_branch_config_key(branch, key):
177 """Helper method to return Git config key for a branch."""
178 assert branch, 'branch name is required to set git config for it'
179 return 'branch.%s.%s' % (branch, key)
180
181
182def _git_get_branch_config_value(key, default=None, value_type=str,
183 branch=False):
184 """Returns git config value of given or current branch if any.
185
186 Returns default in all other cases.
187 """
188 assert value_type in (int, str, bool)
189 if branch is False: # Distinguishing default arg value from None.
190 branch = GetCurrentBranch()
191
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000192 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700193 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000194
tandrii5d48c322016-08-18 16:19:37 -0700195 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700196 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700197 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700198 # git config also has --int, but apparently git config suffers from integer
199 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700200 args.append(_git_branch_config_key(branch, key))
201 code, out = RunGitWithCode(args)
202 if code == 0:
203 value = out.strip()
204 if value_type == int:
205 return int(value)
206 if value_type == bool:
207 return bool(value.lower() == 'true')
208 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000209 return default
210
211
tandrii5d48c322016-08-18 16:19:37 -0700212def _git_set_branch_config_value(key, value, branch=None, **kwargs):
213 """Sets the value or unsets if it's None of a git branch config.
214
215 Valid, though not necessarily existing, branch must be provided,
216 otherwise currently checked out branch is used.
217 """
218 if not branch:
219 branch = GetCurrentBranch()
220 assert branch, 'a branch name OR currently checked out branch is required'
221 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700222 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700223 if value is None:
224 args.append('--unset')
225 elif isinstance(value, bool):
226 args.append('--bool')
227 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700228 else:
tandrii33a46ff2016-08-23 05:53:40 -0700229 # git config also has --int, but apparently git config suffers from integer
230 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700231 value = str(value)
232 args.append(_git_branch_config_key(branch, key))
233 if value is not None:
234 args.append(value)
235 RunGit(args, **kwargs)
236
237
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100238def _get_committer_timestamp(commit):
239 """Returns unix timestamp as integer of a committer in a commit.
240
241 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
242 """
243 # Git also stores timezone offset, but it only affects visual display,
244 # actual point in time is defined by this timestamp only.
245 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
246
247
248def _git_amend_head(message, committer_timestamp):
249 """Amends commit with new message and desired committer_timestamp.
250
251 Sets committer timezone to UTC.
252 """
253 env = os.environ.copy()
254 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
255 return RunGit(['commit', '--amend', '-m', message], env=env)
256
257
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000258def add_git_similarity(parser):
259 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700260 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000261 help='Sets the percentage that a pair of files need to match in order to'
262 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000263 parser.add_option(
264 '--find-copies', action='store_true',
265 help='Allows git to look for copies.')
266 parser.add_option(
267 '--no-find-copies', action='store_false', dest='find_copies',
268 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000269
270 old_parser_args = parser.parse_args
271 def Parse(args):
272 options, args = old_parser_args(args)
273
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000274 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700275 options.similarity = _git_get_branch_config_value(
276 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000277 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000278 print('Note: Saving similarity of %d%% in git config.'
279 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700280 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000281
iannucci@chromium.org79540052012-10-19 23:15:26 +0000282 options.similarity = max(0, min(options.similarity, 100))
283
284 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700285 options.find_copies = _git_get_branch_config_value(
286 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000287 else:
tandrii5d48c322016-08-18 16:19:37 -0700288 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000289
290 print('Using %d%% similarity for rename/copy detection. '
291 'Override with --similarity.' % options.similarity)
292
293 return options, args
294 parser.parse_args = Parse
295
296
machenbach@chromium.org45453142015-09-15 08:45:22 +0000297def _get_properties_from_options(options):
298 properties = dict(x.split('=', 1) for x in options.properties)
299 for key, val in properties.iteritems():
300 try:
301 properties[key] = json.loads(val)
302 except ValueError:
303 pass # If a value couldn't be evaluated, treat it as a string.
304 return properties
305
306
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000307def _prefix_master(master):
308 """Convert user-specified master name to full master name.
309
310 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
311 name, while the developers always use shortened master name
312 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
313 function does the conversion for buildbucket migration.
314 """
borenet6c0efe62016-10-19 08:13:29 -0700315 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000316 return master
borenet6c0efe62016-10-19 08:13:29 -0700317 return '%s%s' % (MASTER_PREFIX, master)
318
319
320def _unprefix_master(bucket):
321 """Convert bucket name to shortened master name.
322
323 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
324 name, while the developers always use shortened master name
325 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
326 function does the conversion for buildbucket migration.
327 """
328 if bucket.startswith(MASTER_PREFIX):
329 return bucket[len(MASTER_PREFIX):]
330 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000331
332
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000333def _buildbucket_retry(operation_name, http, *args, **kwargs):
334 """Retries requests to buildbucket service and returns parsed json content."""
335 try_count = 0
336 while True:
337 response, content = http.request(*args, **kwargs)
338 try:
339 content_json = json.loads(content)
340 except ValueError:
341 content_json = None
342
343 # Buildbucket could return an error even if status==200.
344 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000345 error = content_json.get('error')
346 if error.get('code') == 403:
347 raise BuildbucketResponseException(
348 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000349 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000350 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000351 raise BuildbucketResponseException(msg)
352
353 if response.status == 200:
354 if not content_json:
355 raise BuildbucketResponseException(
356 'Buildbucket returns invalid json content: %s.\n'
357 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
358 content)
359 return content_json
360 if response.status < 500 or try_count >= 2:
361 raise httplib2.HttpLib2Error(content)
362
363 # status >= 500 means transient failures.
364 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700365 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000366 try_count += 1
367 assert False, 'unreachable'
368
369
qyearsley1fdfcb62016-10-24 13:22:03 -0700370def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700371 """Returns a dict mapping bucket names to builders and tests,
372 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700373 """
qyearsleydd49f942016-10-28 11:57:22 -0700374 # If no bots are listed, we try to get a set of builders and tests based
375 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700376 if not options.bot:
377 change = changelist.GetChange(
378 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700379 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700380 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700381 change=change,
382 changed_files=change.LocalPaths(),
383 repository_root=settings.GetRoot(),
384 default_presubmit=None,
385 project=None,
386 verbose=options.verbose,
387 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700388 if masters is None:
389 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100390 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700391
qyearsley1fdfcb62016-10-24 13:22:03 -0700392 if options.bucket:
393 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700394 if options.master:
395 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700396
qyearsleydd49f942016-10-28 11:57:22 -0700397 # If bots are listed but no master or bucket, then we need to find out
398 # the corresponding master for each bot.
399 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
400 if error_message:
401 option_parser.error(
402 'Tryserver master cannot be found because: %s\n'
403 'Please manually specify the tryserver master, e.g. '
404 '"-m tryserver.chromium.linux".' % error_message)
405 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700406
407
qyearsley123a4682016-10-26 09:12:17 -0700408def _get_bucket_map_for_builders(builders):
409 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700410 map_url = 'https://builders-map.appspot.com/'
411 try:
qyearsley123a4682016-10-26 09:12:17 -0700412 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700413 except urllib2.URLError as e:
414 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
415 (map_url, e))
416 except ValueError as e:
417 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700418 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700419 return None, 'Failed to build master map.'
420
qyearsley123a4682016-10-26 09:12:17 -0700421 bucket_map = {}
422 for builder in builders:
qyearsley123a4682016-10-26 09:12:17 -0700423 masters = builders_map.get(builder, [])
424 if not masters:
qyearsley1fdfcb62016-10-24 13:22:03 -0700425 return None, ('No matching master for builder %s.' % builder)
qyearsley123a4682016-10-26 09:12:17 -0700426 if len(masters) > 1:
qyearsley1fdfcb62016-10-24 13:22:03 -0700427 return None, ('The builder name %s exists in multiple masters %s.' %
qyearsley123a4682016-10-26 09:12:17 -0700428 (builder, masters))
429 bucket = _prefix_master(masters[0])
430 bucket_map.setdefault(bucket, {})[builder] = []
431
432 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700433
434
borenet6c0efe62016-10-19 08:13:29 -0700435def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700436 category='git_cl_try', patchset=None):
qyearsley1fdfcb62016-10-24 13:22:03 -0700437 """Sends a request to Buildbucket to trigger try jobs for a changelist.
438
439 Args:
440 auth_config: AuthConfig for Rietveld.
441 changelist: Changelist that the try jobs are associated with.
442 buckets: A nested dict mapping bucket names to builders to tests.
443 options: Command-line options.
444 """
tandriide281ae2016-10-12 06:02:30 -0700445 assert changelist.GetIssue(), 'CL must be uploaded first'
446 codereview_url = changelist.GetCodereviewServer()
447 assert codereview_url, 'CL must be uploaded first'
448 patchset = patchset or changelist.GetMostRecentPatchset()
449 assert patchset, 'CL must be uploaded first'
450
451 codereview_host = urlparse.urlparse(codereview_url).hostname
452 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000453 http = authenticator.authorize(httplib2.Http())
454 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700455
456 # TODO(tandrii): consider caching Gerrit CL details just like
457 # _RietveldChangelistImpl does, then caching values in these two variables
458 # won't be necessary.
459 owner_email = changelist.GetIssueOwner()
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000460
461 buildbucket_put_url = (
462 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000463 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700464 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
465 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
466 hostname=codereview_host,
467 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000468 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700469
470 shared_parameters_properties = changelist.GetTryjobProperties(patchset)
471 shared_parameters_properties['category'] = category
472 if options.clobber:
473 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700474 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700475 if extra_properties:
476 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000477
478 batch_req_body = {'builds': []}
479 print_text = []
480 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700481 for bucket, builders_and_tests in sorted(buckets.iteritems()):
482 print_text.append('Bucket: %s' % bucket)
483 master = None
484 if bucket.startswith(MASTER_PREFIX):
485 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000486 for builder, tests in sorted(builders_and_tests.iteritems()):
487 print_text.append(' %s: %s' % (builder, tests))
488 parameters = {
489 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000490 'changes': [{
tandriide281ae2016-10-12 06:02:30 -0700491 'author': {'email': owner_email},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000492 'revision': options.revision,
493 }],
tandrii8c5a3532016-11-04 07:52:02 -0700494 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000495 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000496 if 'presubmit' in builder.lower():
497 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000498 if tests:
499 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700500
501 tags = [
502 'builder:%s' % builder,
503 'buildset:%s' % buildset,
504 'user_agent:git_cl_try',
505 ]
506 if master:
507 parameters['properties']['master'] = master
508 tags.append('master:%s' % master)
509
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000510 batch_req_body['builds'].append(
511 {
512 'bucket': bucket,
513 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000514 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700515 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000516 }
517 )
518
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000519 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700520 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000521 http,
522 buildbucket_put_url,
523 'PUT',
524 body=json.dumps(batch_req_body),
525 headers={'Content-Type': 'application/json'}
526 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000527 print_text.append('To see results here, run: git cl try-results')
528 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700529 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000530
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000531
tandrii221ab252016-10-06 08:12:04 -0700532def fetch_try_jobs(auth_config, changelist, buildbucket_host,
533 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700534 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000535
qyearsley53f48a12016-09-01 10:45:13 -0700536 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000537 """
tandrii221ab252016-10-06 08:12:04 -0700538 assert buildbucket_host
539 assert changelist.GetIssue(), 'CL must be uploaded first'
540 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
541 patchset = patchset or changelist.GetMostRecentPatchset()
542 assert patchset, 'CL must be uploaded first'
543
544 codereview_url = changelist.GetCodereviewServer()
545 codereview_host = urlparse.urlparse(codereview_url).hostname
546 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000547 if authenticator.has_cached_credentials():
548 http = authenticator.authorize(httplib2.Http())
549 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700550 print('Warning: Some results might be missing because %s' %
551 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700552 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000553 http = httplib2.Http()
554
555 http.force_exception_to_status_code = True
556
tandrii221ab252016-10-06 08:12:04 -0700557 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
558 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
559 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000560 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700561 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000562 params = {'tag': 'buildset:%s' % buildset}
563
564 builds = {}
565 while True:
566 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700567 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000568 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700569 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000570 for build in content.get('builds', []):
571 builds[build['id']] = build
572 if 'next_cursor' in content:
573 params['start_cursor'] = content['next_cursor']
574 else:
575 break
576 return builds
577
578
qyearsleyeab3c042016-08-24 09:18:28 -0700579def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000580 """Prints nicely result of fetch_try_jobs."""
581 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700582 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000583 return
584
585 # Make a copy, because we'll be modifying builds dictionary.
586 builds = builds.copy()
587 builder_names_cache = {}
588
589 def get_builder(b):
590 try:
591 return builder_names_cache[b['id']]
592 except KeyError:
593 try:
594 parameters = json.loads(b['parameters_json'])
595 name = parameters['builder_name']
596 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700597 print('WARNING: failed to get builder name for build %s: %s' % (
598 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000599 name = None
600 builder_names_cache[b['id']] = name
601 return name
602
603 def get_bucket(b):
604 bucket = b['bucket']
605 if bucket.startswith('master.'):
606 return bucket[len('master.'):]
607 return bucket
608
609 if options.print_master:
610 name_fmt = '%%-%ds %%-%ds' % (
611 max(len(str(get_bucket(b))) for b in builds.itervalues()),
612 max(len(str(get_builder(b))) for b in builds.itervalues()))
613 def get_name(b):
614 return name_fmt % (get_bucket(b), get_builder(b))
615 else:
616 name_fmt = '%%-%ds' % (
617 max(len(str(get_builder(b))) for b in builds.itervalues()))
618 def get_name(b):
619 return name_fmt % get_builder(b)
620
621 def sort_key(b):
622 return b['status'], b.get('result'), get_name(b), b.get('url')
623
624 def pop(title, f, color=None, **kwargs):
625 """Pop matching builds from `builds` dict and print them."""
626
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000627 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000628 colorize = str
629 else:
630 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
631
632 result = []
633 for b in builds.values():
634 if all(b.get(k) == v for k, v in kwargs.iteritems()):
635 builds.pop(b['id'])
636 result.append(b)
637 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700638 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000639 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700640 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000641
642 total = len(builds)
643 pop(status='COMPLETED', result='SUCCESS',
644 title='Successes:', color=Fore.GREEN,
645 f=lambda b: (get_name(b), b.get('url')))
646 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
647 title='Infra Failures:', color=Fore.MAGENTA,
648 f=lambda b: (get_name(b), b.get('url')))
649 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
650 title='Failures:', color=Fore.RED,
651 f=lambda b: (get_name(b), b.get('url')))
652 pop(status='COMPLETED', result='CANCELED',
653 title='Canceled:', color=Fore.MAGENTA,
654 f=lambda b: (get_name(b),))
655 pop(status='COMPLETED', result='FAILURE',
656 failure_reason='INVALID_BUILD_DEFINITION',
657 title='Wrong master/builder name:', color=Fore.MAGENTA,
658 f=lambda b: (get_name(b),))
659 pop(status='COMPLETED', result='FAILURE',
660 title='Other failures:',
661 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
662 pop(status='COMPLETED',
663 title='Other finished:',
664 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
665 pop(status='STARTED',
666 title='Started:', color=Fore.YELLOW,
667 f=lambda b: (get_name(b), b.get('url')))
668 pop(status='SCHEDULED',
669 title='Scheduled:',
670 f=lambda b: (get_name(b), 'id=%s' % b['id']))
671 # The last section is just in case buildbucket API changes OR there is a bug.
672 pop(title='Other:',
673 f=lambda b: (get_name(b), 'id=%s' % b['id']))
674 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700675 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000676
677
qyearsley53f48a12016-09-01 10:45:13 -0700678def write_try_results_json(output_file, builds):
679 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
680
681 The input |builds| dict is assumed to be generated by Buildbucket.
682 Buildbucket documentation: http://goo.gl/G0s101
683 """
684
685 def convert_build_dict(build):
686 return {
687 'buildbucket_id': build.get('id'),
688 'status': build.get('status'),
689 'result': build.get('result'),
690 'bucket': build.get('bucket'),
691 'builder_name': json.loads(
692 build.get('parameters_json', '{}')).get('builder_name'),
693 'failure_reason': build.get('failure_reason'),
694 'url': build.get('url'),
695 }
696
697 converted = []
698 for _, build in sorted(builds.items()):
699 converted.append(convert_build_dict(build))
700 write_json(output_file, converted)
701
702
iannucci@chromium.org79540052012-10-19 23:15:26 +0000703def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000704 """Prints statistics about the change to the user."""
705 # --no-ext-diff is broken in some versions of Git, so try to work around
706 # this by overriding the environment (but there is still a problem if the
707 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000708 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000709 if 'GIT_EXTERNAL_DIFF' in env:
710 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000711
712 if find_copies:
scottmgb84b5e32016-11-10 09:25:33 -0800713 similarity_options = ['-l100000', '-C%s' % similarity]
iannucci@chromium.org79540052012-10-19 23:15:26 +0000714 else:
715 similarity_options = ['-M%s' % similarity]
716
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000717 try:
718 stdout = sys.stdout.fileno()
719 except AttributeError:
720 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000721 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000722 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000723 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000724 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000725
726
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000727class BuildbucketResponseException(Exception):
728 pass
729
730
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000731class Settings(object):
732 def __init__(self):
733 self.default_server = None
734 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000735 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000736 self.tree_status_url = None
737 self.viewvc_url = None
738 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000739 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000740 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000741 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000742 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000743 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000744 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000745 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000746
747 def LazyUpdateIfNeeded(self):
748 """Updates the settings from a codereview.settings file, if available."""
749 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000750 # The only value that actually changes the behavior is
751 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000752 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000753 error_ok=True
754 ).strip().lower()
755
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000756 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000757 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000758 LoadCodereviewSettingsFromFile(cr_settings_file)
759 self.updated = True
760
761 def GetDefaultServerUrl(self, error_ok=False):
762 if not self.default_server:
763 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000764 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000765 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000766 if error_ok:
767 return self.default_server
768 if not self.default_server:
769 error_message = ('Could not find settings file. You must configure '
770 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000771 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000772 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000773 return self.default_server
774
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000775 @staticmethod
776 def GetRelativeRoot():
777 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000778
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000779 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000780 if self.root is None:
781 self.root = os.path.abspath(self.GetRelativeRoot())
782 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000783
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000784 def GetGitMirror(self, remote='origin'):
785 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000786 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000787 if not os.path.isdir(local_url):
788 return None
789 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
790 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
791 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
792 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
793 if mirror.exists():
794 return mirror
795 return None
796
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000797 def GetTreeStatusUrl(self, error_ok=False):
798 if not self.tree_status_url:
799 error_message = ('You must configure your tree status URL by running '
800 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000801 self.tree_status_url = self._GetRietveldConfig(
802 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000803 return self.tree_status_url
804
805 def GetViewVCUrl(self):
806 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000807 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000808 return self.viewvc_url
809
rmistry@google.com90752582014-01-14 21:04:50 +0000810 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000811 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000812
rmistry@google.com78948ed2015-07-08 23:09:57 +0000813 def GetIsSkipDependencyUpload(self, branch_name):
814 """Returns true if specified branch should skip dep uploads."""
815 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
816 error_ok=True)
817
rmistry@google.com5626a922015-02-26 14:03:30 +0000818 def GetRunPostUploadHook(self):
819 run_post_upload_hook = self._GetRietveldConfig(
820 'run-post-upload-hook', error_ok=True)
821 return run_post_upload_hook == "True"
822
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000823 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000824 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000825
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000826 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000827 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000828
ukai@chromium.orge8077812012-02-03 03:41:46 +0000829 def GetIsGerrit(self):
830 """Return true if this repo is assosiated with gerrit code review system."""
831 if self.is_gerrit is None:
832 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
833 return self.is_gerrit
834
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000835 def GetSquashGerritUploads(self):
836 """Return true if uploads to Gerrit should be squashed by default."""
837 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700838 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
839 if self.squash_gerrit_uploads is None:
840 # Default is squash now (http://crbug.com/611892#c23).
841 self.squash_gerrit_uploads = not (
842 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
843 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000844 return self.squash_gerrit_uploads
845
tandriia60502f2016-06-20 02:01:53 -0700846 def GetSquashGerritUploadsOverride(self):
847 """Return True or False if codereview.settings should be overridden.
848
849 Returns None if no override has been defined.
850 """
851 # See also http://crbug.com/611892#c23
852 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
853 error_ok=True).strip()
854 if result == 'true':
855 return True
856 if result == 'false':
857 return False
858 return None
859
tandrii@chromium.org28253532016-04-14 13:46:56 +0000860 def GetGerritSkipEnsureAuthenticated(self):
861 """Return True if EnsureAuthenticated should not be done for Gerrit
862 uploads."""
863 if self.gerrit_skip_ensure_authenticated is None:
864 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000865 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000866 error_ok=True).strip() == 'true')
867 return self.gerrit_skip_ensure_authenticated
868
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000869 def GetGitEditor(self):
870 """Return the editor specified in the git config, or None if none is."""
871 if self.git_editor is None:
872 self.git_editor = self._GetConfig('core.editor', error_ok=True)
873 return self.git_editor or None
874
thestig@chromium.org44202a22014-03-11 19:22:18 +0000875 def GetLintRegex(self):
876 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
877 DEFAULT_LINT_REGEX)
878
879 def GetLintIgnoreRegex(self):
880 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
881 DEFAULT_LINT_IGNORE_REGEX)
882
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000883 def GetProject(self):
884 if not self.project:
885 self.project = self._GetRietveldConfig('project', error_ok=True)
886 return self.project
887
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000888 def GetForceHttpsCommitUrl(self):
889 if not self.force_https_commit_url:
890 self.force_https_commit_url = self._GetRietveldConfig(
891 'force-https-commit-url', error_ok=True)
892 return self.force_https_commit_url
893
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000894 def GetPendingRefPrefix(self):
895 if not self.pending_ref_prefix:
896 self.pending_ref_prefix = self._GetRietveldConfig(
897 'pending-ref-prefix', error_ok=True)
898 return self.pending_ref_prefix
899
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000900 def _GetRietveldConfig(self, param, **kwargs):
901 return self._GetConfig('rietveld.' + param, **kwargs)
902
rmistry@google.com78948ed2015-07-08 23:09:57 +0000903 def _GetBranchConfig(self, branch_name, param, **kwargs):
904 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
905
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000906 def _GetConfig(self, param, **kwargs):
907 self.LazyUpdateIfNeeded()
908 return RunGit(['config', param], **kwargs).strip()
909
910
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100911class _GitNumbererState(object):
912 KNOWN_PROJECTS_WHITELIST = [
913 'chromium/src',
914 'external/webrtc',
915 'v8/v8',
916 ]
917
918 @classmethod
919 def load(cls, remote_url, remote_ref):
920 """Figures out the state by fetching special refs from remote repo.
921 """
922 assert remote_ref and remote_ref.startswith('refs/'), remote_ref
923 url_parts = urlparse.urlparse(remote_url)
924 project_name = url_parts.path.lstrip('/').rstrip('git./')
925 for known in cls.KNOWN_PROJECTS_WHITELIST:
926 if project_name.endswith(known):
927 break
928 else:
929 # Early exit to avoid extra fetches for repos that aren't using gnumbd.
930 return cls(cls._get_pending_prefix_fallback(), None)
931
Quinten Yearsley442fb642016-12-15 15:38:27 -0800932 # This pollutes local ref space, but the amount of objects is negligible.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100933 error, _ = cls._run_git_with_code([
934 'fetch', remote_url,
935 '+refs/meta/config:refs/git_cl/meta/config',
936 '+refs/gnumbd-config/main:refs/git_cl/gnumbd-config/main'])
937 if error:
938 # Some ref doesn't exist or isn't accessible to current user.
939 # This shouldn't happen on production KNOWN_PROJECTS_WHITELIST
940 # with git-numberer.
941 cls._warn('failed to fetch gnumbd and project config for %s: %s',
942 remote_url, error)
943 return cls(cls._get_pending_prefix_fallback(), None)
944 return cls(cls._get_pending_prefix(remote_ref),
945 cls._is_validator_enabled(remote_ref))
946
947 @classmethod
948 def _get_pending_prefix(cls, ref):
949 error, gnumbd_config_data = cls._run_git_with_code(
950 ['show', 'refs/git_cl/gnumbd-config/main:config.json'])
951 if error:
952 cls._warn('gnumbd config file not found')
953 return cls._get_pending_prefix_fallback()
954
955 try:
956 config = json.loads(gnumbd_config_data)
957 if cls.match_refglobs(ref, config['enabled_refglobs']):
958 return config['pending_ref_prefix']
959 return None
960 except KeyboardInterrupt:
961 raise
962 except Exception as e:
963 cls._warn('failed to parse gnumbd config: %s', e)
964 return cls._get_pending_prefix_fallback()
965
966 @staticmethod
967 def _get_pending_prefix_fallback():
968 global settings
969 if not settings:
970 settings = Settings()
971 return settings.GetPendingRefPrefix()
972
973 @classmethod
974 def _is_validator_enabled(cls, ref):
975 error, project_config_data = cls._run_git_with_code(
976 ['show', 'refs/git_cl/meta/config:project.config'])
977 if error:
978 cls._warn('project.config file not found')
979 return False
980 # Gerrit's project.config is really a git config file.
981 # So, parse it as such.
982 with tempfile.NamedTemporaryFile(prefix='git_cl_proj_config') as f:
983 f.write(project_config_data)
984 # Make sure OS sees this, but don't close the file just yet,
985 # as NamedTemporaryFile deletes it on closing.
986 f.flush()
987
988 def get_opts(x):
989 code, out = cls._run_git_with_code(
990 ['config', '-f', f.name, '--get-all',
991 'plugin.git-numberer.validate-%s-refglob' % x])
992 if code == 0:
993 return out.strip().splitlines()
994 return []
995 enabled, disabled = map(get_opts, ['enabled', 'disabled'])
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100996 logging.info('validator config enabled %s disabled %s refglobs for '
997 '(this ref: %s)', enabled, disabled, ref)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100998
999 if cls.match_refglobs(ref, disabled):
1000 return False
1001 return cls.match_refglobs(ref, enabled)
1002
1003 @staticmethod
1004 def match_refglobs(ref, refglobs):
1005 for refglob in refglobs:
1006 if ref == refglob or fnmatch.fnmatch(ref, refglob):
1007 return True
1008 return False
1009
1010 @staticmethod
1011 def _run_git_with_code(*args, **kwargs):
1012 # The only reason for this wrapper is easy porting of this code to CQ
1013 # codebase, which forked git_cl.py and checkouts.py long time ago.
1014 return RunGitWithCode(*args, **kwargs)
1015
1016 @staticmethod
1017 def _warn(msg, *args):
1018 if args:
1019 msg = msg % args
1020 print('WARNING: %s' % msg)
1021
1022 def __init__(self, pending_prefix, validator_enabled):
1023 # TODO(tandrii): remove pending_prefix after gnumbd is no more.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01001024 if pending_prefix:
1025 if not pending_prefix.endswith('/'):
1026 pending_prefix += '/'
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001027 self._pending_prefix = pending_prefix or None
1028 self._validator_enabled = validator_enabled or False
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01001029 logging.debug('_GitNumbererState(pending: %s, validator: %s)',
1030 self._pending_prefix, self._validator_enabled)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001031
1032 @property
1033 def pending_prefix(self):
1034 return self._pending_prefix
1035
1036 @property
Andrii Shyshkalov8f15f3e2016-12-14 15:43:49 +01001037 def should_add_git_number(self):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001038 return self._validator_enabled and self._pending_prefix is None
1039
1040
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001041def ShortBranchName(branch):
1042 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001043 return branch.replace('refs/heads/', '', 1)
1044
1045
1046def GetCurrentBranchRef():
1047 """Returns branch ref (e.g., refs/heads/master) or None."""
1048 return RunGit(['symbolic-ref', 'HEAD'],
1049 stderr=subprocess2.VOID, error_ok=True).strip() or None
1050
1051
1052def GetCurrentBranch():
1053 """Returns current branch or None.
1054
1055 For refs/heads/* branches, returns just last part. For others, full ref.
1056 """
1057 branchref = GetCurrentBranchRef()
1058 if branchref:
1059 return ShortBranchName(branchref)
1060 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001061
1062
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001063class _CQState(object):
1064 """Enum for states of CL with respect to Commit Queue."""
1065 NONE = 'none'
1066 DRY_RUN = 'dry_run'
1067 COMMIT = 'commit'
1068
1069 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1070
1071
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001072class _ParsedIssueNumberArgument(object):
1073 def __init__(self, issue=None, patchset=None, hostname=None):
1074 self.issue = issue
1075 self.patchset = patchset
1076 self.hostname = hostname
1077
1078 @property
1079 def valid(self):
1080 return self.issue is not None
1081
1082
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001083def ParseIssueNumberArgument(arg):
1084 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1085 fail_result = _ParsedIssueNumberArgument()
1086
1087 if arg.isdigit():
1088 return _ParsedIssueNumberArgument(issue=int(arg))
1089 if not arg.startswith('http'):
1090 return fail_result
1091 url = gclient_utils.UpgradeToHttps(arg)
1092 try:
1093 parsed_url = urlparse.urlparse(url)
1094 except ValueError:
1095 return fail_result
1096 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
1097 tmp = cls.ParseIssueURL(parsed_url)
1098 if tmp is not None:
1099 return tmp
1100 return fail_result
1101
1102
Aaron Gablea45ee112016-11-22 15:14:38 -08001103class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001104 def __init__(self, issue, url):
1105 self.issue = issue
1106 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001107 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001108
1109 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001110 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001111 self.issue, self.url)
1112
1113
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001114class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001115 """Changelist works with one changelist in local branch.
1116
1117 Supports two codereview backends: Rietveld or Gerrit, selected at object
1118 creation.
1119
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001120 Notes:
1121 * Not safe for concurrent multi-{thread,process} use.
1122 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001123 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001124 """
1125
1126 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1127 """Create a new ChangeList instance.
1128
1129 If issue is given, the codereview must be given too.
1130
1131 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1132 Otherwise, it's decided based on current configuration of the local branch,
1133 with default being 'rietveld' for backwards compatibility.
1134 See _load_codereview_impl for more details.
1135
1136 **kwargs will be passed directly to codereview implementation.
1137 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001138 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001139 global settings
1140 if not settings:
1141 # Happens when git_cl.py is used as a utility library.
1142 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001143
1144 if issue:
1145 assert codereview, 'codereview must be known, if issue is known'
1146
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001147 self.branchref = branchref
1148 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001149 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001150 self.branch = ShortBranchName(self.branchref)
1151 else:
1152 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001153 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001154 self.lookedup_issue = False
1155 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001156 self.has_description = False
1157 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001158 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001159 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001160 self.cc = None
1161 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001162 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001163
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001164 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001165 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001166 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001167 assert self._codereview_impl
1168 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001169
1170 def _load_codereview_impl(self, codereview=None, **kwargs):
1171 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001172 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1173 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1174 self._codereview = codereview
1175 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001176 return
1177
1178 # Automatic selection based on issue number set for a current branch.
1179 # Rietveld takes precedence over Gerrit.
1180 assert not self.issue
1181 # Whether we find issue or not, we are doing the lookup.
1182 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001183 if self.GetBranch():
1184 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1185 issue = _git_get_branch_config_value(
1186 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1187 if issue:
1188 self._codereview = codereview
1189 self._codereview_impl = cls(self, **kwargs)
1190 self.issue = int(issue)
1191 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001192
1193 # No issue is set for this branch, so decide based on repo-wide settings.
1194 return self._load_codereview_impl(
1195 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1196 **kwargs)
1197
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001198 def IsGerrit(self):
1199 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001200
1201 def GetCCList(self):
1202 """Return the users cc'd on this CL.
1203
agable92bec4f2016-08-24 09:27:27 -07001204 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001205 """
1206 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001207 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001208 more_cc = ','.join(self.watchers)
1209 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1210 return self.cc
1211
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001212 def GetCCListWithoutDefault(self):
1213 """Return the users cc'd on this CL excluding default ones."""
1214 if self.cc is None:
1215 self.cc = ','.join(self.watchers)
1216 return self.cc
1217
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001218 def SetWatchers(self, watchers):
1219 """Set the list of email addresses that should be cc'd based on the changed
1220 files in this CL.
1221 """
1222 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001223
1224 def GetBranch(self):
1225 """Returns the short branch name, e.g. 'master'."""
1226 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001227 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001228 if not branchref:
1229 return None
1230 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001231 self.branch = ShortBranchName(self.branchref)
1232 return self.branch
1233
1234 def GetBranchRef(self):
1235 """Returns the full branch name, e.g. 'refs/heads/master'."""
1236 self.GetBranch() # Poke the lazy loader.
1237 return self.branchref
1238
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001239 def ClearBranch(self):
1240 """Clears cached branch data of this object."""
1241 self.branch = self.branchref = None
1242
tandrii5d48c322016-08-18 16:19:37 -07001243 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1244 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1245 kwargs['branch'] = self.GetBranch()
1246 return _git_get_branch_config_value(key, default, **kwargs)
1247
1248 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1249 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1250 assert self.GetBranch(), (
1251 'this CL must have an associated branch to %sset %s%s' %
1252 ('un' if value is None else '',
1253 key,
1254 '' if value is None else ' to %r' % value))
1255 kwargs['branch'] = self.GetBranch()
1256 return _git_set_branch_config_value(key, value, **kwargs)
1257
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001258 @staticmethod
1259 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001260 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001261 e.g. 'origin', 'refs/heads/master'
1262 """
1263 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001264 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1265
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001266 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001267 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001268 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001269 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1270 error_ok=True).strip()
1271 if upstream_branch:
1272 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001273 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001274 # Else, try to guess the origin remote.
1275 remote_branches = RunGit(['branch', '-r']).split()
1276 if 'origin/master' in remote_branches:
1277 # Fall back on origin/master if it exits.
1278 remote = 'origin'
1279 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001280 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001281 DieWithError(
1282 'Unable to determine default branch to diff against.\n'
1283 'Either pass complete "git diff"-style arguments, like\n'
1284 ' git cl upload origin/master\n'
1285 'or verify this branch is set up to track another \n'
1286 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001287
1288 return remote, upstream_branch
1289
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001290 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001291 upstream_branch = self.GetUpstreamBranch()
1292 if not BranchExists(upstream_branch):
1293 DieWithError('The upstream for the current branch (%s) does not exist '
1294 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001295 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001296 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001297
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001298 def GetUpstreamBranch(self):
1299 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001300 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001301 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001302 upstream_branch = upstream_branch.replace('refs/heads/',
1303 'refs/remotes/%s/' % remote)
1304 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1305 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001306 self.upstream_branch = upstream_branch
1307 return self.upstream_branch
1308
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001309 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001310 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001311 remote, branch = None, self.GetBranch()
1312 seen_branches = set()
1313 while branch not in seen_branches:
1314 seen_branches.add(branch)
1315 remote, branch = self.FetchUpstreamTuple(branch)
1316 branch = ShortBranchName(branch)
1317 if remote != '.' or branch.startswith('refs/remotes'):
1318 break
1319 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001320 remotes = RunGit(['remote'], error_ok=True).split()
1321 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001322 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001323 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001324 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001325 logging.warn('Could not determine which remote this change is '
1326 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001327 else:
1328 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001329 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001330 branch = 'HEAD'
1331 if branch.startswith('refs/remotes'):
1332 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001333 elif branch.startswith('refs/branch-heads/'):
1334 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001335 else:
1336 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001337 return self._remote
1338
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001339 def GitSanityChecks(self, upstream_git_obj):
1340 """Checks git repo status and ensures diff is from local commits."""
1341
sbc@chromium.org79706062015-01-14 21:18:12 +00001342 if upstream_git_obj is None:
1343 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001344 print('ERROR: unable to determine current branch (detached HEAD?)',
1345 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001346 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001347 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001348 return False
1349
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001350 # Verify the commit we're diffing against is in our current branch.
1351 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1352 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1353 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001354 print('ERROR: %s is not in the current branch. You may need to rebase '
1355 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001356 return False
1357
1358 # List the commits inside the diff, and verify they are all local.
1359 commits_in_diff = RunGit(
1360 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1361 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1362 remote_branch = remote_branch.strip()
1363 if code != 0:
1364 _, remote_branch = self.GetRemoteBranch()
1365
1366 commits_in_remote = RunGit(
1367 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1368
1369 common_commits = set(commits_in_diff) & set(commits_in_remote)
1370 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001371 print('ERROR: Your diff contains %d commits already in %s.\n'
1372 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1373 'the diff. If you are using a custom git flow, you can override'
1374 ' the reference used for this check with "git config '
1375 'gitcl.remotebranch <git-ref>".' % (
1376 len(common_commits), remote_branch, upstream_git_obj),
1377 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001378 return False
1379 return True
1380
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001381 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001382 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001383
1384 Returns None if it is not set.
1385 """
tandrii5d48c322016-08-18 16:19:37 -07001386 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001387
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001388 def GetRemoteUrl(self):
1389 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1390
1391 Returns None if there is no remote.
1392 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001393 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001394 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1395
1396 # If URL is pointing to a local directory, it is probably a git cache.
1397 if os.path.isdir(url):
1398 url = RunGit(['config', 'remote.%s.url' % remote],
1399 error_ok=True,
1400 cwd=url).strip()
1401 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001402
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001403 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001404 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001405 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001406 self.issue = self._GitGetBranchConfigValue(
1407 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001408 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001409 return self.issue
1410
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001411 def GetIssueURL(self):
1412 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001413 issue = self.GetIssue()
1414 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001415 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001416 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001417
1418 def GetDescription(self, pretty=False):
1419 if not self.has_description:
1420 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001421 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001422 self.has_description = True
1423 if pretty:
1424 wrapper = textwrap.TextWrapper()
1425 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1426 return wrapper.fill(self.description)
1427 return self.description
1428
1429 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001430 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001431 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001432 self.patchset = self._GitGetBranchConfigValue(
1433 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001434 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001435 return self.patchset
1436
1437 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001438 """Set this branch's patchset. If patchset=0, clears the patchset."""
1439 assert self.GetBranch()
1440 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001441 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001442 else:
1443 self.patchset = int(patchset)
1444 self._GitSetBranchConfigValue(
1445 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001446
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001447 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001448 """Set this branch's issue. If issue isn't given, clears the issue."""
1449 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001450 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001451 issue = int(issue)
1452 self._GitSetBranchConfigValue(
1453 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001454 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001455 codereview_server = self._codereview_impl.GetCodereviewServer()
1456 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001457 self._GitSetBranchConfigValue(
1458 self._codereview_impl.CodereviewServerConfigKey(),
1459 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001460 else:
tandrii5d48c322016-08-18 16:19:37 -07001461 # Reset all of these just to be clean.
1462 reset_suffixes = [
1463 'last-upload-hash',
1464 self._codereview_impl.IssueConfigKey(),
1465 self._codereview_impl.PatchsetConfigKey(),
1466 self._codereview_impl.CodereviewServerConfigKey(),
1467 ] + self._PostUnsetIssueProperties()
1468 for prop in reset_suffixes:
1469 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001470 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001471 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001472
dnjba1b0f32016-09-02 12:37:42 -07001473 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001474 if not self.GitSanityChecks(upstream_branch):
1475 DieWithError('\nGit sanity check failure')
1476
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001477 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001478 if not root:
1479 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001480 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001481
1482 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001483 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001484 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001485 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001486 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001487 except subprocess2.CalledProcessError:
1488 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001489 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001490 'This branch probably doesn\'t exist anymore. To reset the\n'
1491 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001492 ' git branch --set-upstream-to origin/master %s\n'
1493 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001494 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001495
maruel@chromium.org52424302012-08-29 15:14:30 +00001496 issue = self.GetIssue()
1497 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001498 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001499 description = self.GetDescription()
1500 else:
1501 # If the change was never uploaded, use the log messages of all commits
1502 # up to the branch point, as git cl upload will prefill the description
1503 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001504 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1505 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001506
1507 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001508 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001509 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001510 name,
1511 description,
1512 absroot,
1513 files,
1514 issue,
1515 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001516 author,
1517 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001518
dsansomee2d6fd92016-09-08 00:10:47 -07001519 def UpdateDescription(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001520 self.description = description
dsansomee2d6fd92016-09-08 00:10:47 -07001521 return self._codereview_impl.UpdateDescriptionRemote(
1522 description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001523
1524 def RunHook(self, committing, may_prompt, verbose, change):
1525 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1526 try:
1527 return presubmit_support.DoPresubmitChecks(change, committing,
1528 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1529 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001530 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1531 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001532 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001533 DieWithError(
1534 ('%s\nMaybe your depot_tools is out of date?\n'
1535 'If all fails, contact maruel@') % e)
1536
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001537 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1538 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001539 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1540 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001541 else:
1542 # Assume url.
1543 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1544 urlparse.urlparse(issue_arg))
1545 if not parsed_issue_arg or not parsed_issue_arg.valid:
1546 DieWithError('Failed to parse issue argument "%s". '
1547 'Must be an issue number or a valid URL.' % issue_arg)
1548 return self._codereview_impl.CMDPatchWithParsedIssue(
1549 parsed_issue_arg, reject, nocommit, directory)
1550
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001551 def CMDUpload(self, options, git_diff_args, orig_args):
1552 """Uploads a change to codereview."""
1553 if git_diff_args:
1554 # TODO(ukai): is it ok for gerrit case?
1555 base_branch = git_diff_args[0]
1556 else:
1557 if self.GetBranch() is None:
1558 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1559
1560 # Default to diffing against common ancestor of upstream branch
1561 base_branch = self.GetCommonAncestorWithUpstream()
1562 git_diff_args = [base_branch, 'HEAD']
1563
1564 # Make sure authenticated to codereview before running potentially expensive
1565 # hooks. It is a fast, best efforts check. Codereview still can reject the
1566 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001567 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001568
1569 # Apply watchlists on upload.
1570 change = self.GetChange(base_branch, None)
1571 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1572 files = [f.LocalPath() for f in change.AffectedFiles()]
1573 if not options.bypass_watchlists:
1574 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1575
1576 if not options.bypass_hooks:
1577 if options.reviewers or options.tbr_owners:
1578 # Set the reviewer list now so that presubmit checks can access it.
1579 change_description = ChangeDescription(change.FullDescriptionText())
1580 change_description.update_reviewers(options.reviewers,
1581 options.tbr_owners,
1582 change)
1583 change.SetDescriptionText(change_description.description)
1584 hook_results = self.RunHook(committing=False,
1585 may_prompt=not options.force,
1586 verbose=options.verbose,
1587 change=change)
1588 if not hook_results.should_continue():
1589 return 1
1590 if not options.reviewers and hook_results.reviewers:
1591 options.reviewers = hook_results.reviewers.split(',')
1592
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001593 # TODO(tandrii): Checking local patchset against remote patchset is only
1594 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1595 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001596 latest_patchset = self.GetMostRecentPatchset()
1597 local_patchset = self.GetPatchset()
1598 if (latest_patchset and local_patchset and
1599 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001600 print('The last upload made from this repository was patchset #%d but '
1601 'the most recent patchset on the server is #%d.'
1602 % (local_patchset, latest_patchset))
1603 print('Uploading will still work, but if you\'ve uploaded to this '
1604 'issue from another machine or branch the patch you\'re '
1605 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001606 ask_for_data('About to upload; enter to confirm.')
1607
1608 print_stats(options.similarity, options.find_copies, git_diff_args)
1609 ret = self.CMDUploadChange(options, git_diff_args, change)
1610 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001611 if options.use_commit_queue:
1612 self.SetCQState(_CQState.COMMIT)
1613 elif options.cq_dry_run:
1614 self.SetCQState(_CQState.DRY_RUN)
1615
tandrii5d48c322016-08-18 16:19:37 -07001616 _git_set_branch_config_value('last-upload-hash',
1617 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001618 # Run post upload hooks, if specified.
1619 if settings.GetRunPostUploadHook():
1620 presubmit_support.DoPostUploadExecuter(
1621 change,
1622 self,
1623 settings.GetRoot(),
1624 options.verbose,
1625 sys.stdout)
1626
1627 # Upload all dependencies if specified.
1628 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001629 print()
1630 print('--dependencies has been specified.')
1631 print('All dependent local branches will be re-uploaded.')
1632 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001633 # Remove the dependencies flag from args so that we do not end up in a
1634 # loop.
1635 orig_args.remove('--dependencies')
1636 ret = upload_branch_deps(self, orig_args)
1637 return ret
1638
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001639 def SetCQState(self, new_state):
1640 """Update the CQ state for latest patchset.
1641
1642 Issue must have been already uploaded and known.
1643 """
1644 assert new_state in _CQState.ALL_STATES
1645 assert self.GetIssue()
1646 return self._codereview_impl.SetCQState(new_state)
1647
qyearsley1fdfcb62016-10-24 13:22:03 -07001648 def TriggerDryRun(self):
1649 """Triggers a dry run and prints a warning on failure."""
1650 # TODO(qyearsley): Either re-use this method in CMDset_commit
1651 # and CMDupload, or change CMDtry to trigger dry runs with
1652 # just SetCQState, and catch keyboard interrupt and other
1653 # errors in that method.
1654 try:
1655 self.SetCQState(_CQState.DRY_RUN)
1656 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1657 return 0
1658 except KeyboardInterrupt:
1659 raise
1660 except:
1661 print('WARNING: failed to trigger CQ Dry Run.\n'
1662 'Either:\n'
1663 ' * your project has no CQ\n'
1664 ' * you don\'t have permission to trigger Dry Run\n'
1665 ' * bug in this code (see stack trace below).\n'
1666 'Consider specifying which bots to trigger manually '
1667 'or asking your project owners for permissions '
1668 'or contacting Chrome Infrastructure team at '
1669 'https://www.chromium.org/infra\n\n')
1670 # Still raise exception so that stack trace is printed.
1671 raise
1672
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001673 # Forward methods to codereview specific implementation.
1674
1675 def CloseIssue(self):
1676 return self._codereview_impl.CloseIssue()
1677
1678 def GetStatus(self):
1679 return self._codereview_impl.GetStatus()
1680
1681 def GetCodereviewServer(self):
1682 return self._codereview_impl.GetCodereviewServer()
1683
tandriide281ae2016-10-12 06:02:30 -07001684 def GetIssueOwner(self):
1685 """Get owner from codereview, which may differ from this checkout."""
1686 return self._codereview_impl.GetIssueOwner()
1687
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001688 def GetApprovingReviewers(self):
1689 return self._codereview_impl.GetApprovingReviewers()
1690
1691 def GetMostRecentPatchset(self):
1692 return self._codereview_impl.GetMostRecentPatchset()
1693
tandriide281ae2016-10-12 06:02:30 -07001694 def CannotTriggerTryJobReason(self):
1695 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1696 return self._codereview_impl.CannotTriggerTryJobReason()
1697
tandrii8c5a3532016-11-04 07:52:02 -07001698 def GetTryjobProperties(self, patchset=None):
1699 """Returns dictionary of properties to launch tryjob."""
1700 return self._codereview_impl.GetTryjobProperties(patchset=patchset)
1701
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001702 def __getattr__(self, attr):
1703 # This is because lots of untested code accesses Rietveld-specific stuff
1704 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001705 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001706 # Note that child method defines __getattr__ as well, and forwards it here,
1707 # because _RietveldChangelistImpl is not cleaned up yet, and given
1708 # deprecation of Rietveld, it should probably be just removed.
1709 # Until that time, avoid infinite recursion by bypassing __getattr__
1710 # of implementation class.
1711 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001712
1713
1714class _ChangelistCodereviewBase(object):
1715 """Abstract base class encapsulating codereview specifics of a changelist."""
1716 def __init__(self, changelist):
1717 self._changelist = changelist # instance of Changelist
1718
1719 def __getattr__(self, attr):
1720 # Forward methods to changelist.
1721 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1722 # _RietveldChangelistImpl to avoid this hack?
1723 return getattr(self._changelist, attr)
1724
1725 def GetStatus(self):
1726 """Apply a rough heuristic to give a simple summary of an issue's review
1727 or CQ status, assuming adherence to a common workflow.
1728
1729 Returns None if no issue for this branch, or specific string keywords.
1730 """
1731 raise NotImplementedError()
1732
1733 def GetCodereviewServer(self):
1734 """Returns server URL without end slash, like "https://codereview.com"."""
1735 raise NotImplementedError()
1736
1737 def FetchDescription(self):
1738 """Fetches and returns description from the codereview server."""
1739 raise NotImplementedError()
1740
tandrii5d48c322016-08-18 16:19:37 -07001741 @classmethod
1742 def IssueConfigKey(cls):
1743 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001744 raise NotImplementedError()
1745
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001746 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001747 def PatchsetConfigKey(cls):
1748 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001749 raise NotImplementedError()
1750
tandrii5d48c322016-08-18 16:19:37 -07001751 @classmethod
1752 def CodereviewServerConfigKey(cls):
1753 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001754 raise NotImplementedError()
1755
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001756 def _PostUnsetIssueProperties(self):
1757 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001758 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001759
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001760 def GetRieveldObjForPresubmit(self):
1761 # This is an unfortunate Rietveld-embeddedness in presubmit.
1762 # For non-Rietveld codereviews, this probably should return a dummy object.
1763 raise NotImplementedError()
1764
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001765 def GetGerritObjForPresubmit(self):
1766 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1767 return None
1768
dsansomee2d6fd92016-09-08 00:10:47 -07001769 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001770 """Update the description on codereview site."""
1771 raise NotImplementedError()
1772
1773 def CloseIssue(self):
1774 """Closes the issue."""
1775 raise NotImplementedError()
1776
1777 def GetApprovingReviewers(self):
1778 """Returns a list of reviewers approving the change.
1779
1780 Note: not necessarily committers.
1781 """
1782 raise NotImplementedError()
1783
1784 def GetMostRecentPatchset(self):
1785 """Returns the most recent patchset number from the codereview site."""
1786 raise NotImplementedError()
1787
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001788 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1789 directory):
1790 """Fetches and applies the issue.
1791
1792 Arguments:
1793 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1794 reject: if True, reject the failed patch instead of switching to 3-way
1795 merge. Rietveld only.
1796 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1797 only.
1798 directory: switch to directory before applying the patch. Rietveld only.
1799 """
1800 raise NotImplementedError()
1801
1802 @staticmethod
1803 def ParseIssueURL(parsed_url):
1804 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1805 failed."""
1806 raise NotImplementedError()
1807
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001808 def EnsureAuthenticated(self, force):
1809 """Best effort check that user is authenticated with codereview server.
1810
1811 Arguments:
1812 force: whether to skip confirmation questions.
1813 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001814 raise NotImplementedError()
1815
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001816 def CMDUploadChange(self, options, args, change):
1817 """Uploads a change to codereview."""
1818 raise NotImplementedError()
1819
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001820 def SetCQState(self, new_state):
1821 """Update the CQ state for latest patchset.
1822
1823 Issue must have been already uploaded and known.
1824 """
1825 raise NotImplementedError()
1826
tandriie113dfd2016-10-11 10:20:12 -07001827 def CannotTriggerTryJobReason(self):
1828 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1829 raise NotImplementedError()
1830
tandriide281ae2016-10-12 06:02:30 -07001831 def GetIssueOwner(self):
1832 raise NotImplementedError()
1833
tandrii8c5a3532016-11-04 07:52:02 -07001834 def GetTryjobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001835 raise NotImplementedError()
1836
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001837
1838class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1839 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1840 super(_RietveldChangelistImpl, self).__init__(changelist)
1841 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001842 if not rietveld_server:
1843 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001844
1845 self._rietveld_server = rietveld_server
1846 self._auth_config = auth_config
1847 self._props = None
1848 self._rpc_server = None
1849
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001850 def GetCodereviewServer(self):
1851 if not self._rietveld_server:
1852 # If we're on a branch then get the server potentially associated
1853 # with that branch.
1854 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001855 self._rietveld_server = gclient_utils.UpgradeToHttps(
1856 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001857 if not self._rietveld_server:
1858 self._rietveld_server = settings.GetDefaultServerUrl()
1859 return self._rietveld_server
1860
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001861 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001862 """Best effort check that user is authenticated with Rietveld server."""
1863 if self._auth_config.use_oauth2:
1864 authenticator = auth.get_authenticator_for_host(
1865 self.GetCodereviewServer(), self._auth_config)
1866 if not authenticator.has_cached_credentials():
1867 raise auth.LoginRequiredError(self.GetCodereviewServer())
1868
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001869 def FetchDescription(self):
1870 issue = self.GetIssue()
1871 assert issue
1872 try:
1873 return self.RpcServer().get_description(issue).strip()
1874 except urllib2.HTTPError as e:
1875 if e.code == 404:
1876 DieWithError(
1877 ('\nWhile fetching the description for issue %d, received a '
1878 '404 (not found)\n'
1879 'error. It is likely that you deleted this '
1880 'issue on the server. If this is the\n'
1881 'case, please run\n\n'
1882 ' git cl issue 0\n\n'
1883 'to clear the association with the deleted issue. Then run '
1884 'this command again.') % issue)
1885 else:
1886 DieWithError(
1887 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1888 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001889 print('Warning: Failed to retrieve CL description due to network '
1890 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001891 return ''
1892
1893 def GetMostRecentPatchset(self):
1894 return self.GetIssueProperties()['patchsets'][-1]
1895
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001896 def GetIssueProperties(self):
1897 if self._props is None:
1898 issue = self.GetIssue()
1899 if not issue:
1900 self._props = {}
1901 else:
1902 self._props = self.RpcServer().get_issue_properties(issue, True)
1903 return self._props
1904
tandriie113dfd2016-10-11 10:20:12 -07001905 def CannotTriggerTryJobReason(self):
1906 props = self.GetIssueProperties()
1907 if not props:
1908 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1909 if props.get('closed'):
1910 return 'CL %s is closed' % self.GetIssue()
1911 if props.get('private'):
1912 return 'CL %s is private' % self.GetIssue()
1913 return None
1914
tandrii8c5a3532016-11-04 07:52:02 -07001915 def GetTryjobProperties(self, patchset=None):
1916 """Returns dictionary of properties to launch tryjob."""
1917 project = (self.GetIssueProperties() or {}).get('project')
1918 return {
1919 'issue': self.GetIssue(),
1920 'patch_project': project,
1921 'patch_storage': 'rietveld',
1922 'patchset': patchset or self.GetPatchset(),
1923 'rietveld': self.GetCodereviewServer(),
1924 }
1925
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001926 def GetApprovingReviewers(self):
1927 return get_approving_reviewers(self.GetIssueProperties())
1928
tandriide281ae2016-10-12 06:02:30 -07001929 def GetIssueOwner(self):
1930 return (self.GetIssueProperties() or {}).get('owner_email')
1931
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001932 def AddComment(self, message):
1933 return self.RpcServer().add_comment(self.GetIssue(), message)
1934
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001935 def GetStatus(self):
1936 """Apply a rough heuristic to give a simple summary of an issue's review
1937 or CQ status, assuming adherence to a common workflow.
1938
1939 Returns None if no issue for this branch, or one of the following keywords:
1940 * 'error' - error from review tool (including deleted issues)
1941 * 'unsent' - not sent for review
1942 * 'waiting' - waiting for review
1943 * 'reply' - waiting for owner to reply to review
1944 * 'lgtm' - LGTM from at least one approved reviewer
1945 * 'commit' - in the commit queue
1946 * 'closed' - closed
1947 """
1948 if not self.GetIssue():
1949 return None
1950
1951 try:
1952 props = self.GetIssueProperties()
1953 except urllib2.HTTPError:
1954 return 'error'
1955
1956 if props.get('closed'):
1957 # Issue is closed.
1958 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001959 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001960 # Issue is in the commit queue.
1961 return 'commit'
1962
1963 try:
1964 reviewers = self.GetApprovingReviewers()
1965 except urllib2.HTTPError:
1966 return 'error'
1967
1968 if reviewers:
1969 # Was LGTM'ed.
1970 return 'lgtm'
1971
1972 messages = props.get('messages') or []
1973
tandrii9d2c7a32016-06-22 03:42:45 -07001974 # Skip CQ messages that don't require owner's action.
1975 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1976 if 'Dry run:' in messages[-1]['text']:
1977 messages.pop()
1978 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1979 # This message always follows prior messages from CQ,
1980 # so skip this too.
1981 messages.pop()
1982 else:
1983 # This is probably a CQ messages warranting user attention.
1984 break
1985
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001986 if not messages:
1987 # No message was sent.
1988 return 'unsent'
1989 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001990 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001991 return 'reply'
1992 return 'waiting'
1993
dsansomee2d6fd92016-09-08 00:10:47 -07001994 def UpdateDescriptionRemote(self, description, force=False):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001995 return self.RpcServer().update_description(
1996 self.GetIssue(), self.description)
1997
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001998 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001999 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002000
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002001 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002002 return self.SetFlags({flag: value})
2003
2004 def SetFlags(self, flags):
2005 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002006 """
phajdan.jr68598232016-08-10 03:28:28 -07002007 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002008 try:
tandrii4b233bd2016-07-06 03:50:29 -07002009 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002010 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002011 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002012 if e.code == 404:
2013 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2014 if e.code == 403:
2015 DieWithError(
2016 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002017 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002018 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002019
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002020 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002021 """Returns an upload.RpcServer() to access this review's rietveld instance.
2022 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002023 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002024 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002025 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002026 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002027 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002028
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002029 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002030 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002031 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002032
tandrii5d48c322016-08-18 16:19:37 -07002033 @classmethod
2034 def PatchsetConfigKey(cls):
2035 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002036
tandrii5d48c322016-08-18 16:19:37 -07002037 @classmethod
2038 def CodereviewServerConfigKey(cls):
2039 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002040
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002041 def GetRieveldObjForPresubmit(self):
2042 return self.RpcServer()
2043
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002044 def SetCQState(self, new_state):
2045 props = self.GetIssueProperties()
2046 if props.get('private'):
2047 DieWithError('Cannot set-commit on private issue')
2048
2049 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002050 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002051 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002052 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002053 else:
tandrii4b233bd2016-07-06 03:50:29 -07002054 assert new_state == _CQState.DRY_RUN
2055 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002056
2057
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002058 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2059 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002060 # PatchIssue should never be called with a dirty tree. It is up to the
2061 # caller to check this, but just in case we assert here since the
2062 # consequences of the caller not checking this could be dire.
2063 assert(not git_common.is_dirty_git_tree('apply'))
2064 assert(parsed_issue_arg.valid)
2065 self._changelist.issue = parsed_issue_arg.issue
2066 if parsed_issue_arg.hostname:
2067 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2068
skobes6468b902016-10-24 08:45:10 -07002069 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2070 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2071 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002072 try:
skobes6468b902016-10-24 08:45:10 -07002073 scm_obj.apply_patch(patchset_object)
2074 except Exception as e:
2075 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002076 return 1
2077
2078 # If we had an issue, commit the current state and register the issue.
2079 if not nocommit:
2080 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2081 'patch from issue %(i)s at patchset '
2082 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2083 % {'i': self.GetIssue(), 'p': patchset})])
2084 self.SetIssue(self.GetIssue())
2085 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07002086 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002087 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002088 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002089 return 0
2090
2091 @staticmethod
2092 def ParseIssueURL(parsed_url):
2093 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2094 return None
wychen3c1c1722016-08-04 11:46:36 -07002095 # Rietveld patch: https://domain/<number>/#ps<patchset>
2096 match = re.match(r'/(\d+)/$', parsed_url.path)
2097 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2098 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002099 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002100 issue=int(match.group(1)),
2101 patchset=int(match2.group(1)),
2102 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002103 # Typical url: https://domain/<issue_number>[/[other]]
2104 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2105 if match:
skobes6468b902016-10-24 08:45:10 -07002106 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002107 issue=int(match.group(1)),
2108 hostname=parsed_url.netloc)
2109 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2110 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2111 if match:
skobes6468b902016-10-24 08:45:10 -07002112 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002113 issue=int(match.group(1)),
2114 patchset=int(match.group(2)),
skobes6468b902016-10-24 08:45:10 -07002115 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002116 return None
2117
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002118 def CMDUploadChange(self, options, args, change):
2119 """Upload the patch to Rietveld."""
2120 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2121 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002122 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2123 if options.emulate_svn_auto_props:
2124 upload_args.append('--emulate_svn_auto_props')
2125
2126 change_desc = None
2127
2128 if options.email is not None:
2129 upload_args.extend(['--email', options.email])
2130
2131 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002132 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002133 upload_args.extend(['--title', options.title])
2134 if options.message:
2135 upload_args.extend(['--message', options.message])
2136 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002137 print('This branch is associated with issue %s. '
2138 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002139 else:
nodirca166002016-06-27 10:59:51 -07002140 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002141 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002142 if options.message:
2143 message = options.message
2144 else:
2145 message = CreateDescriptionFromLog(args)
2146 if options.title:
2147 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002148 change_desc = ChangeDescription(message)
2149 if options.reviewers or options.tbr_owners:
2150 change_desc.update_reviewers(options.reviewers,
2151 options.tbr_owners,
2152 change)
2153 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002154 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002155
2156 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002157 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002158 return 1
2159
2160 upload_args.extend(['--message', change_desc.description])
2161 if change_desc.get_reviewers():
2162 upload_args.append('--reviewers=%s' % ','.join(
2163 change_desc.get_reviewers()))
2164 if options.send_mail:
2165 if not change_desc.get_reviewers():
2166 DieWithError("Must specify reviewers to send email.")
2167 upload_args.append('--send_mail')
2168
2169 # We check this before applying rietveld.private assuming that in
2170 # rietveld.cc only addresses which we can send private CLs to are listed
2171 # if rietveld.private is set, and so we should ignore rietveld.cc only
2172 # when --private is specified explicitly on the command line.
2173 if options.private:
2174 logging.warn('rietveld.cc is ignored since private flag is specified. '
2175 'You need to review and add them manually if necessary.')
2176 cc = self.GetCCListWithoutDefault()
2177 else:
2178 cc = self.GetCCList()
2179 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002180 if change_desc.get_cced():
2181 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002182 if cc:
2183 upload_args.extend(['--cc', cc])
2184
2185 if options.private or settings.GetDefaultPrivateFlag() == "True":
2186 upload_args.append('--private')
2187
2188 upload_args.extend(['--git_similarity', str(options.similarity)])
2189 if not options.find_copies:
2190 upload_args.extend(['--git_no_find_copies'])
2191
2192 # Include the upstream repo's URL in the change -- this is useful for
2193 # projects that have their source spread across multiple repos.
2194 remote_url = self.GetGitBaseUrlFromConfig()
2195 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002196 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2197 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2198 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002199 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002200 remote, remote_branch = self.GetRemoteBranch()
2201 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01002202 pending_prefix_check=True,
2203 remote_url=self.GetRemoteUrl())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002204 if target_ref:
2205 upload_args.extend(['--target_ref', target_ref])
2206
2207 # Look for dependent patchsets. See crbug.com/480453 for more details.
2208 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2209 upstream_branch = ShortBranchName(upstream_branch)
2210 if remote is '.':
2211 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002212 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002213 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002214 print()
2215 print('Skipping dependency patchset upload because git config '
2216 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2217 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002218 else:
2219 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002220 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002221 auth_config=auth_config)
2222 branch_cl_issue_url = branch_cl.GetIssueURL()
2223 branch_cl_issue = branch_cl.GetIssue()
2224 branch_cl_patchset = branch_cl.GetPatchset()
2225 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2226 upload_args.extend(
2227 ['--depends_on_patchset', '%s:%s' % (
2228 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002229 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002230 '\n'
2231 'The current branch (%s) is tracking a local branch (%s) with '
2232 'an associated CL.\n'
2233 'Adding %s/#ps%s as a dependency patchset.\n'
2234 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2235 branch_cl_patchset))
2236
2237 project = settings.GetProject()
2238 if project:
2239 upload_args.extend(['--project', project])
2240
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002241 try:
2242 upload_args = ['upload'] + upload_args + args
2243 logging.info('upload.RealMain(%s)', upload_args)
2244 issue, patchset = upload.RealMain(upload_args)
2245 issue = int(issue)
2246 patchset = int(patchset)
2247 except KeyboardInterrupt:
2248 sys.exit(1)
2249 except:
2250 # If we got an exception after the user typed a description for their
2251 # change, back up the description before re-raising.
2252 if change_desc:
2253 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2254 print('\nGot exception while uploading -- saving description to %s\n' %
2255 backup_path)
2256 backup_file = open(backup_path, 'w')
2257 backup_file.write(change_desc.description)
2258 backup_file.close()
2259 raise
2260
2261 if not self.GetIssue():
2262 self.SetIssue(issue)
2263 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002264 return 0
2265
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002266
2267class _GerritChangelistImpl(_ChangelistCodereviewBase):
2268 def __init__(self, changelist, auth_config=None):
2269 # auth_config is Rietveld thing, kept here to preserve interface only.
2270 super(_GerritChangelistImpl, self).__init__(changelist)
2271 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002272 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002273 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002274 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002275
2276 def _GetGerritHost(self):
2277 # Lazy load of configs.
2278 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002279 if self._gerrit_host and '.' not in self._gerrit_host:
2280 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2281 # This happens for internal stuff http://crbug.com/614312.
2282 parsed = urlparse.urlparse(self.GetRemoteUrl())
2283 if parsed.scheme == 'sso':
2284 print('WARNING: using non https URLs for remote is likely broken\n'
2285 ' Your current remote is: %s' % self.GetRemoteUrl())
2286 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2287 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002288 return self._gerrit_host
2289
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002290 def _GetGitHost(self):
2291 """Returns git host to be used when uploading change to Gerrit."""
2292 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2293
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002294 def GetCodereviewServer(self):
2295 if not self._gerrit_server:
2296 # If we're on a branch then get the server potentially associated
2297 # with that branch.
2298 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002299 self._gerrit_server = self._GitGetBranchConfigValue(
2300 self.CodereviewServerConfigKey())
2301 if self._gerrit_server:
2302 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002303 if not self._gerrit_server:
2304 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2305 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002306 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002307 parts[0] = parts[0] + '-review'
2308 self._gerrit_host = '.'.join(parts)
2309 self._gerrit_server = 'https://%s' % self._gerrit_host
2310 return self._gerrit_server
2311
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002312 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002313 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002314 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002315
tandrii5d48c322016-08-18 16:19:37 -07002316 @classmethod
2317 def PatchsetConfigKey(cls):
2318 return 'gerritpatchset'
2319
2320 @classmethod
2321 def CodereviewServerConfigKey(cls):
2322 return 'gerritserver'
2323
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002324 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002325 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002326 if settings.GetGerritSkipEnsureAuthenticated():
2327 # For projects with unusual authentication schemes.
2328 # See http://crbug.com/603378.
2329 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002330 # Lazy-loader to identify Gerrit and Git hosts.
2331 if gerrit_util.GceAuthenticator.is_gce():
2332 return
2333 self.GetCodereviewServer()
2334 git_host = self._GetGitHost()
2335 assert self._gerrit_server and self._gerrit_host
2336 cookie_auth = gerrit_util.CookiesAuthenticator()
2337
2338 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2339 git_auth = cookie_auth.get_auth_header(git_host)
2340 if gerrit_auth and git_auth:
2341 if gerrit_auth == git_auth:
2342 return
2343 print((
2344 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2345 ' Check your %s or %s file for credentials of hosts:\n'
2346 ' %s\n'
2347 ' %s\n'
2348 ' %s') %
2349 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2350 git_host, self._gerrit_host,
2351 cookie_auth.get_new_password_message(git_host)))
2352 if not force:
2353 ask_for_data('If you know what you are doing, press Enter to continue, '
2354 'Ctrl+C to abort.')
2355 return
2356 else:
2357 missing = (
2358 [] if gerrit_auth else [self._gerrit_host] +
2359 [] if git_auth else [git_host])
2360 DieWithError('Credentials for the following hosts are required:\n'
2361 ' %s\n'
2362 'These are read from %s (or legacy %s)\n'
2363 '%s' % (
2364 '\n '.join(missing),
2365 cookie_auth.get_gitcookies_path(),
2366 cookie_auth.get_netrc_path(),
2367 cookie_auth.get_new_password_message(git_host)))
2368
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002369 def _PostUnsetIssueProperties(self):
2370 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002371 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002372
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002373 def GetRieveldObjForPresubmit(self):
2374 class ThisIsNotRietveldIssue(object):
2375 def __nonzero__(self):
2376 # This is a hack to make presubmit_support think that rietveld is not
2377 # defined, yet still ensure that calls directly result in a decent
2378 # exception message below.
2379 return False
2380
2381 def __getattr__(self, attr):
2382 print(
2383 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2384 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2385 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2386 'or use Rietveld for codereview.\n'
2387 'See also http://crbug.com/579160.' % attr)
2388 raise NotImplementedError()
2389 return ThisIsNotRietveldIssue()
2390
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002391 def GetGerritObjForPresubmit(self):
2392 return presubmit_support.GerritAccessor(self._GetGerritHost())
2393
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002394 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002395 """Apply a rough heuristic to give a simple summary of an issue's review
2396 or CQ status, assuming adherence to a common workflow.
2397
2398 Returns None if no issue for this branch, or one of the following keywords:
2399 * 'error' - error from review tool (including deleted issues)
2400 * 'unsent' - no reviewers added
2401 * 'waiting' - waiting for review
2402 * 'reply' - waiting for owner to reply to review
Quinten Yearsley442fb642016-12-15 15:38:27 -08002403 * 'not lgtm' - Code-Review disapproval from at least one valid reviewer
tandriic2405f52016-10-10 08:13:15 -07002404 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002405 * 'commit' - in the commit queue
2406 * 'closed' - abandoned
2407 """
2408 if not self.GetIssue():
2409 return None
2410
2411 try:
2412 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002413 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002414 return 'error'
2415
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002416 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002417 return 'closed'
2418
2419 cq_label = data['labels'].get('Commit-Queue', {})
2420 if cq_label:
rmistryc9ebbd22016-10-14 12:35:54 -07002421 votes = cq_label.get('all', [])
2422 highest_vote = 0
2423 for v in votes:
2424 highest_vote = max(highest_vote, v.get('value', 0))
2425 vote_value = str(highest_vote)
2426 if vote_value != '0':
2427 # Add a '+' if the value is not 0 to match the values in the label.
2428 # The cq_label does not have negatives.
2429 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002430 vote_text = cq_label.get('values', {}).get(vote_value, '')
2431 if vote_text.lower() == 'commit':
2432 return 'commit'
2433
2434 lgtm_label = data['labels'].get('Code-Review', {})
2435 if lgtm_label:
2436 if 'rejected' in lgtm_label:
2437 return 'not lgtm'
2438 if 'approved' in lgtm_label:
2439 return 'lgtm'
2440
2441 if not data.get('reviewers', {}).get('REVIEWER', []):
2442 return 'unsent'
2443
2444 messages = data.get('messages', [])
2445 if messages:
2446 owner = data['owner'].get('_account_id')
2447 last_message_author = messages[-1].get('author', {}).get('_account_id')
2448 if owner != last_message_author:
2449 # Some reply from non-owner.
2450 return 'reply'
2451
2452 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002453
2454 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002455 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002456 return data['revisions'][data['current_revision']]['_number']
2457
2458 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002459 data = self._GetChangeDetail(['CURRENT_REVISION'])
2460 current_rev = data['current_revision']
2461 url = data['revisions'][current_rev]['fetch']['http']['url']
2462 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002463
dsansomee2d6fd92016-09-08 00:10:47 -07002464 def UpdateDescriptionRemote(self, description, force=False):
2465 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2466 if not force:
2467 ask_for_data(
2468 'The description cannot be modified while the issue has a pending '
2469 'unpublished edit. Either publish the edit in the Gerrit web UI '
2470 'or delete it.\n\n'
2471 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2472
2473 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2474 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002475 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002476 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002477
2478 def CloseIssue(self):
2479 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2480
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002481 def GetApprovingReviewers(self):
2482 """Returns a list of reviewers approving the change.
2483
2484 Note: not necessarily committers.
2485 """
2486 raise NotImplementedError()
2487
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002488 def SubmitIssue(self, wait_for_merge=True):
2489 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2490 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002491
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002492 def _GetChangeDetail(self, options=None, issue=None):
2493 options = options or []
2494 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002495 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002496 try:
2497 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2498 options, ignore_404=False)
2499 except gerrit_util.GerritError as e:
2500 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002501 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002502 raise
tandriic2405f52016-10-10 08:13:15 -07002503 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002504
agable32978d92016-11-01 12:55:02 -07002505 def _GetChangeCommit(self, issue=None):
2506 issue = issue or self.GetIssue()
2507 assert issue, 'issue is required to query Gerrit'
2508 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2509 if not data:
Aaron Gablea45ee112016-11-22 15:14:38 -08002510 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
agable32978d92016-11-01 12:55:02 -07002511 return data
2512
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002513 def CMDLand(self, force, bypass_hooks, verbose):
2514 if git_common.is_dirty_git_tree('land'):
2515 return 1
tandriid60367b2016-06-22 05:25:12 -07002516 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2517 if u'Commit-Queue' in detail.get('labels', {}):
2518 if not force:
2519 ask_for_data('\nIt seems this repository has a Commit Queue, '
2520 'which can test and land changes for you. '
2521 'Are you sure you wish to bypass it?\n'
2522 'Press Enter to continue, Ctrl+C to abort.')
2523
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002524 differs = True
tandriic4344b52016-08-29 06:04:54 -07002525 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002526 # Note: git diff outputs nothing if there is no diff.
2527 if not last_upload or RunGit(['diff', last_upload]).strip():
2528 print('WARNING: some changes from local branch haven\'t been uploaded')
2529 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002530 if detail['current_revision'] == last_upload:
2531 differs = False
2532 else:
2533 print('WARNING: local branch contents differ from latest uploaded '
2534 'patchset')
2535 if differs:
2536 if not force:
2537 ask_for_data(
Andrii Shyshkalov3aac7752016-11-16 15:23:13 +01002538 'Do you want to submit latest Gerrit patchset and bypass hooks?\n'
2539 'Press Enter to continue, Ctrl+C to abort.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002540 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2541 elif not bypass_hooks:
2542 hook_results = self.RunHook(
2543 committing=True,
2544 may_prompt=not force,
2545 verbose=verbose,
2546 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2547 if not hook_results.should_continue():
2548 return 1
2549
2550 self.SubmitIssue(wait_for_merge=True)
2551 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002552 links = self._GetChangeCommit().get('web_links', [])
2553 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002554 if link.get('name') == 'gitiles' and link.get('url'):
agable32978d92016-11-01 12:55:02 -07002555 print('Landed as %s' % link.get('url'))
2556 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002557 return 0
2558
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002559 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2560 directory):
2561 assert not reject
2562 assert not nocommit
2563 assert not directory
2564 assert parsed_issue_arg.valid
2565
2566 self._changelist.issue = parsed_issue_arg.issue
2567
2568 if parsed_issue_arg.hostname:
2569 self._gerrit_host = parsed_issue_arg.hostname
2570 self._gerrit_server = 'https://%s' % self._gerrit_host
2571
tandriic2405f52016-10-10 08:13:15 -07002572 try:
2573 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002574 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002575 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002576
2577 if not parsed_issue_arg.patchset:
2578 # Use current revision by default.
2579 revision_info = detail['revisions'][detail['current_revision']]
2580 patchset = int(revision_info['_number'])
2581 else:
2582 patchset = parsed_issue_arg.patchset
2583 for revision_info in detail['revisions'].itervalues():
2584 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2585 break
2586 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002587 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002588 (parsed_issue_arg.patchset, self.GetIssue()))
2589
2590 fetch_info = revision_info['fetch']['http']
2591 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2592 RunGit(['cherry-pick', 'FETCH_HEAD'])
2593 self.SetIssue(self.GetIssue())
2594 self.SetPatchset(patchset)
Aaron Gablea45ee112016-11-22 15:14:38 -08002595 print('Committed patch for change %i patchset %i locally' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002596 (self.GetIssue(), self.GetPatchset()))
2597 return 0
2598
2599 @staticmethod
2600 def ParseIssueURL(parsed_url):
2601 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2602 return None
2603 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2604 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2605 # Short urls like https://domain/<issue_number> can be used, but don't allow
2606 # specifying the patchset (you'd 404), but we allow that here.
2607 if parsed_url.path == '/':
2608 part = parsed_url.fragment
2609 else:
2610 part = parsed_url.path
2611 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2612 if match:
2613 return _ParsedIssueNumberArgument(
2614 issue=int(match.group(2)),
2615 patchset=int(match.group(4)) if match.group(4) else None,
2616 hostname=parsed_url.netloc)
2617 return None
2618
tandrii16e0b4e2016-06-07 10:34:28 -07002619 def _GerritCommitMsgHookCheck(self, offer_removal):
2620 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2621 if not os.path.exists(hook):
2622 return
2623 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2624 # custom developer made one.
2625 data = gclient_utils.FileRead(hook)
2626 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2627 return
2628 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002629 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002630 'and may interfere with it in subtle ways.\n'
2631 'We recommend you remove the commit-msg hook.')
2632 if offer_removal:
2633 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2634 if reply.lower().startswith('y'):
2635 gclient_utils.rm_file_or_tree(hook)
2636 print('Gerrit commit-msg hook removed.')
2637 else:
2638 print('OK, will keep Gerrit commit-msg hook in place.')
2639
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002640 def CMDUploadChange(self, options, args, change):
2641 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002642 if options.squash and options.no_squash:
2643 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002644
2645 if not options.squash and not options.no_squash:
2646 # Load default for user, repo, squash=true, in this order.
2647 options.squash = settings.GetSquashGerritUploads()
2648 elif options.no_squash:
2649 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002650
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002651 # We assume the remote called "origin" is the one we want.
2652 # It is probably not worthwhile to support different workflows.
2653 gerrit_remote = 'origin'
2654
2655 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01002656 # Gerrit will not support pending prefix at all.
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002657 branch = GetTargetRef(remote, remote_branch, options.target_branch,
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01002658 pending_prefix_check=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002659
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002660 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002661 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002662 if self.GetIssue():
2663 # Try to get the message from a previous upload.
2664 message = self.GetDescription()
2665 if not message:
2666 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002667 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002668 '%s' % (self.GetIssue(), self.GetIssueURL()))
2669 change_id = self._GetChangeDetail()['change_id']
2670 while True:
2671 footer_change_ids = git_footers.get_footer_change_id(message)
2672 if footer_change_ids == [change_id]:
2673 break
2674 if not footer_change_ids:
2675 message = git_footers.add_footer_change_id(message, change_id)
Aaron Gablea45ee112016-11-22 15:14:38 -08002676 print('WARNING: appended missing Change-Id to change description')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002677 continue
2678 # There is already a valid footer but with different or several ids.
2679 # Doing this automatically is non-trivial as we don't want to lose
2680 # existing other footers, yet we want to append just 1 desired
2681 # Change-Id. Thus, just create a new footer, but let user verify the
2682 # new description.
2683 message = '%s\n\nChange-Id: %s' % (message, change_id)
2684 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002685 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002686 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002687 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002688 'Please, check the proposed correction to the description, '
2689 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2690 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2691 change_id))
2692 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2693 if not options.force:
2694 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002695 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002696 message = change_desc.description
2697 if not message:
2698 DieWithError("Description is empty. Aborting...")
2699 # Continue the while loop.
2700 # Sanity check of this code - we should end up with proper message
2701 # footer.
2702 assert [change_id] == git_footers.get_footer_change_id(message)
2703 change_desc = ChangeDescription(message)
2704 else:
2705 change_desc = ChangeDescription(
2706 options.message or CreateDescriptionFromLog(args))
2707 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002708 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002709 if not change_desc.description:
2710 DieWithError("Description is empty. Aborting...")
2711 message = change_desc.description
2712 change_ids = git_footers.get_footer_change_id(message)
2713 if len(change_ids) > 1:
2714 DieWithError('too many Change-Id footers, at most 1 allowed.')
2715 if not change_ids:
2716 # Generate the Change-Id automatically.
2717 message = git_footers.add_footer_change_id(
2718 message, GenerateGerritChangeId(message))
2719 change_desc.set_description(message)
2720 change_ids = git_footers.get_footer_change_id(message)
2721 assert len(change_ids) == 1
2722 change_id = change_ids[0]
2723
2724 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2725 if remote is '.':
2726 # If our upstream branch is local, we base our squashed commit on its
2727 # squashed version.
2728 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2729 # Check the squashed hash of the parent.
2730 parent = RunGit(['config',
2731 'branch.%s.gerritsquashhash' % upstream_branch_name],
2732 error_ok=True).strip()
2733 # Verify that the upstream branch has been uploaded too, otherwise
2734 # Gerrit will create additional CLs when uploading.
2735 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2736 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002737 DieWithError(
2738 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002739 'Note: maybe you\'ve uploaded it with --no-squash. '
2740 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002741 ' git cl upload --squash\n' % upstream_branch_name)
2742 else:
2743 parent = self.GetCommonAncestorWithUpstream()
2744
2745 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2746 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2747 '-m', message]).strip()
2748 else:
2749 change_desc = ChangeDescription(
2750 options.message or CreateDescriptionFromLog(args))
2751 if not change_desc.description:
2752 DieWithError("Description is empty. Aborting...")
2753
2754 if not git_footers.get_footer_change_id(change_desc.description):
2755 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002756 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2757 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002758 ref_to_push = 'HEAD'
2759 parent = '%s/%s' % (gerrit_remote, branch)
2760 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2761
2762 assert change_desc
2763 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2764 ref_to_push)]).splitlines()
2765 if len(commits) > 1:
2766 print('WARNING: This will upload %d commits. Run the following command '
2767 'to see which commits will be uploaded: ' % len(commits))
2768 print('git log %s..%s' % (parent, ref_to_push))
2769 print('You can also use `git squash-branch` to squash these into a '
2770 'single commit.')
2771 ask_for_data('About to upload; enter to confirm.')
2772
2773 if options.reviewers or options.tbr_owners:
2774 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2775 change)
2776
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002777 # Extra options that can be specified at push time. Doc:
2778 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2779 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002780 if change_desc.get_reviewers(tbr_only=True):
2781 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2782 refspec_opts.append('l=Code-Review+1')
2783
Aaron Gable9b713dd2016-12-14 16:04:21 -08002784 title = options.title
2785 if not title:
2786 if self.GetIssue():
2787 # We already have an issue, so we should ask for a title for new patch.
2788 default = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
2789 title = ask_for_data('Title for patchset [%s]: ' % default) or default
2790 else:
2791 title = 'Initial upload'
2792 if title:
2793 if not re.match(r'^[\w ]+$', title):
2794 title = re.sub(r'[^\w ]', '', title)
tandriieefe8322016-08-17 10:12:24 -07002795 print('WARNING: Patchset title may only contain alphanumeric chars '
Aaron Gable9b713dd2016-12-14 16:04:21 -08002796 'and spaces. Cleaned up title:\n%s' % title)
tandriieefe8322016-08-17 10:12:24 -07002797 if not options.force:
2798 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002799 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2800 # reverse on its side.
Aaron Gable9b713dd2016-12-14 16:04:21 -08002801 refspec_opts.append('m=' + title.replace(' ', '_'))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002802
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002803 if options.send_mail:
2804 if not change_desc.get_reviewers():
2805 DieWithError('Must specify reviewers to send email.')
2806 refspec_opts.append('notify=ALL')
2807 else:
2808 refspec_opts.append('notify=NONE')
2809
tandrii99a72f22016-08-17 14:33:24 -07002810 reviewers = change_desc.get_reviewers()
2811 if reviewers:
2812 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002813
agablec6787972016-09-09 16:13:34 -07002814 if options.private:
2815 refspec_opts.append('draft')
2816
rmistry9eadede2016-09-19 11:22:43 -07002817 if options.topic:
2818 # Documentation on Gerrit topics is here:
2819 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2820 refspec_opts.append('topic=%s' % options.topic)
2821
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002822 refspec_suffix = ''
2823 if refspec_opts:
2824 refspec_suffix = '%' + ','.join(refspec_opts)
2825 assert ' ' not in refspec_suffix, (
2826 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002827 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002828
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002829 try:
2830 push_stdout = gclient_utils.CheckCallAndFilter(
2831 ['git', 'push', gerrit_remote, refspec],
2832 print_stdout=True,
2833 # Flush after every line: useful for seeing progress when running as
2834 # recipe.
2835 filter_fn=lambda _: sys.stdout.flush())
2836 except subprocess2.CalledProcessError:
2837 DieWithError('Failed to create a change. Please examine output above '
2838 'for the reason of the failure. ')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002839
2840 if options.squash:
2841 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2842 change_numbers = [m.group(1)
2843 for m in map(regex.match, push_stdout.splitlines())
2844 if m]
2845 if len(change_numbers) != 1:
2846 DieWithError(
2847 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2848 'Change-Id: %s') % (len(change_numbers), change_id))
2849 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002850 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002851
2852 # Add cc's from the CC_LIST and --cc flag (if any).
2853 cc = self.GetCCList().split(',')
2854 if options.cc:
2855 cc.extend(options.cc)
2856 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002857 if change_desc.get_cced():
2858 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002859 if cc:
Aaron Gable5db82092016-11-17 10:52:08 -08002860 gerrit_util.AddReviewers(
tandrii88189772016-09-29 04:29:57 -07002861 self._GetGerritHost(), self.GetIssue(), cc, is_reviewer=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002862 return 0
2863
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002864 def _AddChangeIdToCommitMessage(self, options, args):
2865 """Re-commits using the current message, assumes the commit hook is in
2866 place.
2867 """
2868 log_desc = options.message or CreateDescriptionFromLog(args)
2869 git_command = ['commit', '--amend', '-m', log_desc]
2870 RunGit(git_command)
2871 new_log_desc = CreateDescriptionFromLog(args)
2872 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002873 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002874 return new_log_desc
2875 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002876 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002877
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002878 def SetCQState(self, new_state):
2879 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002880 vote_map = {
2881 _CQState.NONE: 0,
2882 _CQState.DRY_RUN: 1,
2883 _CQState.COMMIT : 2,
2884 }
Andrii Shyshkalov828701b2016-12-09 10:46:47 +01002885 kwargs = {'labels': {'Commit-Queue': vote_map[new_state]}}
2886 if new_state == _CQState.DRY_RUN:
2887 # Don't spam everybody reviewer/owner.
2888 kwargs['notify'] = 'NONE'
2889 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(), **kwargs)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002890
tandriie113dfd2016-10-11 10:20:12 -07002891 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002892 try:
2893 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002894 except GerritChangeNotExists:
2895 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002896
2897 if data['status'] in ('ABANDONED', 'MERGED'):
2898 return 'CL %s is closed' % self.GetIssue()
2899
2900 def GetTryjobProperties(self, patchset=None):
2901 """Returns dictionary of properties to launch tryjob."""
2902 data = self._GetChangeDetail(['ALL_REVISIONS'])
2903 patchset = int(patchset or self.GetPatchset())
2904 assert patchset
2905 revision_data = None # Pylint wants it to be defined.
2906 for revision_data in data['revisions'].itervalues():
2907 if int(revision_data['_number']) == patchset:
2908 break
2909 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002910 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002911 (patchset, self.GetIssue()))
2912 return {
2913 'patch_issue': self.GetIssue(),
2914 'patch_set': patchset or self.GetPatchset(),
2915 'patch_project': data['project'],
2916 'patch_storage': 'gerrit',
2917 'patch_ref': revision_data['fetch']['http']['ref'],
2918 'patch_repository_url': revision_data['fetch']['http']['url'],
2919 'patch_gerrit_url': self.GetCodereviewServer(),
2920 }
tandriie113dfd2016-10-11 10:20:12 -07002921
tandriide281ae2016-10-12 06:02:30 -07002922 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002923 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002924
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002925
2926_CODEREVIEW_IMPLEMENTATIONS = {
2927 'rietveld': _RietveldChangelistImpl,
2928 'gerrit': _GerritChangelistImpl,
2929}
2930
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002931
iannuccie53c9352016-08-17 14:40:40 -07002932def _add_codereview_issue_select_options(parser, extra=""):
2933 _add_codereview_select_options(parser)
2934
2935 text = ('Operate on this issue number instead of the current branch\'s '
2936 'implicit issue.')
2937 if extra:
2938 text += ' '+extra
2939 parser.add_option('-i', '--issue', type=int, help=text)
2940
2941
2942def _process_codereview_issue_select_options(parser, options):
2943 _process_codereview_select_options(parser, options)
2944 if options.issue is not None and not options.forced_codereview:
2945 parser.error('--issue must be specified with either --rietveld or --gerrit')
2946
2947
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002948def _add_codereview_select_options(parser):
2949 """Appends --gerrit and --rietveld options to force specific codereview."""
2950 parser.codereview_group = optparse.OptionGroup(
2951 parser, 'EXPERIMENTAL! Codereview override options')
2952 parser.add_option_group(parser.codereview_group)
2953 parser.codereview_group.add_option(
2954 '--gerrit', action='store_true',
2955 help='Force the use of Gerrit for codereview')
2956 parser.codereview_group.add_option(
2957 '--rietveld', action='store_true',
2958 help='Force the use of Rietveld for codereview')
2959
2960
2961def _process_codereview_select_options(parser, options):
2962 if options.gerrit and options.rietveld:
2963 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2964 options.forced_codereview = None
2965 if options.gerrit:
2966 options.forced_codereview = 'gerrit'
2967 elif options.rietveld:
2968 options.forced_codereview = 'rietveld'
2969
2970
tandriif9aefb72016-07-01 09:06:51 -07002971def _get_bug_line_values(default_project, bugs):
2972 """Given default_project and comma separated list of bugs, yields bug line
2973 values.
2974
2975 Each bug can be either:
2976 * a number, which is combined with default_project
2977 * string, which is left as is.
2978
2979 This function may produce more than one line, because bugdroid expects one
2980 project per line.
2981
2982 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2983 ['v8:123', 'chromium:789']
2984 """
2985 default_bugs = []
2986 others = []
2987 for bug in bugs.split(','):
2988 bug = bug.strip()
2989 if bug:
2990 try:
2991 default_bugs.append(int(bug))
2992 except ValueError:
2993 others.append(bug)
2994
2995 if default_bugs:
2996 default_bugs = ','.join(map(str, default_bugs))
2997 if default_project:
2998 yield '%s:%s' % (default_project, default_bugs)
2999 else:
3000 yield default_bugs
3001 for other in sorted(others):
3002 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3003 yield other
3004
3005
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003006class ChangeDescription(object):
3007 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003008 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003009 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00003010 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003011 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003012
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003013 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003014 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003015
agable@chromium.org42c20792013-09-12 17:34:49 +00003016 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003017 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003018 return '\n'.join(self._description_lines)
3019
3020 def set_description(self, desc):
3021 if isinstance(desc, basestring):
3022 lines = desc.splitlines()
3023 else:
3024 lines = [line.rstrip() for line in desc]
3025 while lines and not lines[0]:
3026 lines.pop(0)
3027 while lines and not lines[-1]:
3028 lines.pop(-1)
3029 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003030
piman@chromium.org336f9122014-09-04 02:16:55 +00003031 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003032 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003033 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00003034 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003035 return
agable@chromium.org42c20792013-09-12 17:34:49 +00003036 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003037
agable@chromium.org42c20792013-09-12 17:34:49 +00003038 # Get the set of R= and TBR= lines and remove them from the desciption.
3039 regexp = re.compile(self.R_LINE)
3040 matches = [regexp.match(line) for line in self._description_lines]
3041 new_desc = [l for i, l in enumerate(self._description_lines)
3042 if not matches[i]]
3043 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003044
agable@chromium.org42c20792013-09-12 17:34:49 +00003045 # Construct new unified R= and TBR= lines.
3046 r_names = []
3047 tbr_names = []
3048 for match in matches:
3049 if not match:
3050 continue
3051 people = cleanup_list([match.group(2).strip()])
3052 if match.group(1) == 'TBR':
3053 tbr_names.extend(people)
3054 else:
3055 r_names.extend(people)
3056 for name in r_names:
3057 if name not in reviewers:
3058 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003059 if add_owners_tbr:
3060 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003061 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003062 all_reviewers = set(tbr_names + reviewers)
3063 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3064 all_reviewers)
3065 tbr_names.extend(owners_db.reviewers_for(missing_files,
3066 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003067 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3068 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3069
3070 # Put the new lines in the description where the old first R= line was.
3071 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3072 if 0 <= line_loc < len(self._description_lines):
3073 if new_tbr_line:
3074 self._description_lines.insert(line_loc, new_tbr_line)
3075 if new_r_line:
3076 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003077 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003078 if new_r_line:
3079 self.append_footer(new_r_line)
3080 if new_tbr_line:
3081 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003082
tandriif9aefb72016-07-01 09:06:51 -07003083 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003084 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003085 self.set_description([
3086 '# Enter a description of the change.',
3087 '# This will be displayed on the codereview site.',
3088 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003089 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003090 '--------------------',
3091 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003092
agable@chromium.org42c20792013-09-12 17:34:49 +00003093 regexp = re.compile(self.BUG_LINE)
3094 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003095 prefix = settings.GetBugPrefix()
3096 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
3097 for value in values:
3098 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
3099 self.append_footer('BUG=%s' % value)
3100
agable@chromium.org42c20792013-09-12 17:34:49 +00003101 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003102 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003103 if not content:
3104 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003105 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003106
3107 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003108 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3109 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003110 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003111 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003112
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003113 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003114 """Adds a footer line to the description.
3115
3116 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3117 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3118 that Gerrit footers are always at the end.
3119 """
3120 parsed_footer_line = git_footers.parse_footer(line)
3121 if parsed_footer_line:
3122 # Line is a gerrit footer in the form: Footer-Key: any value.
3123 # Thus, must be appended observing Gerrit footer rules.
3124 self.set_description(
3125 git_footers.add_footer(self.description,
3126 key=parsed_footer_line[0],
3127 value=parsed_footer_line[1]))
3128 return
3129
3130 if not self._description_lines:
3131 self._description_lines.append(line)
3132 return
3133
3134 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3135 if gerrit_footers:
3136 # git_footers.split_footers ensures that there is an empty line before
3137 # actual (gerrit) footers, if any. We have to keep it that way.
3138 assert top_lines and top_lines[-1] == ''
3139 top_lines, separator = top_lines[:-1], top_lines[-1:]
3140 else:
3141 separator = [] # No need for separator if there are no gerrit_footers.
3142
3143 prev_line = top_lines[-1] if top_lines else ''
3144 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3145 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3146 top_lines.append('')
3147 top_lines.append(line)
3148 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003149
tandrii99a72f22016-08-17 14:33:24 -07003150 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003151 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003152 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003153 reviewers = [match.group(2).strip()
3154 for match in matches
3155 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003156 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003157
bradnelsond975b302016-10-23 12:20:23 -07003158 def get_cced(self):
3159 """Retrieves the list of reviewers."""
3160 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3161 cced = [match.group(2).strip() for match in matches if match]
3162 return cleanup_list(cced)
3163
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003164 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3165 """Updates this commit description given the parent.
3166
3167 This is essentially what Gnumbd used to do.
3168 Consult https://goo.gl/WMmpDe for more details.
3169 """
3170 assert parent_msg # No, orphan branch creation isn't supported.
3171 assert parent_hash
3172 assert dest_ref
3173 parent_footer_map = git_footers.parse_footers(parent_msg)
3174 # This will also happily parse svn-position, which GnumbD is no longer
3175 # supporting. While we'd generate correct footers, the verifier plugin
3176 # installed in Gerrit will block such commit (ie git push below will fail).
3177 parent_position = git_footers.get_position(parent_footer_map)
3178
3179 # Cherry-picks may have last line obscuring their prior footers,
3180 # from git_footers perspective. This is also what Gnumbd did.
3181 cp_line = None
3182 if (self._description_lines and
3183 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3184 cp_line = self._description_lines.pop()
3185
3186 top_lines, _, parsed_footers = git_footers.split_footers(self.description)
3187
3188 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3189 # user interference with actual footers we'd insert below.
3190 for i, (k, v) in enumerate(parsed_footers):
3191 if k.startswith('Cr-'):
3192 parsed_footers[i] = (k.replace('Cr-', 'Cr-Original-'), v)
3193
3194 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003195 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003196 if parent_position[0] == dest_ref:
3197 # Same branch as parent.
3198 number = int(parent_position[1]) + 1
3199 else:
3200 number = 1 # New branch, and extra lineage.
3201 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3202 int(parent_position[1])))
3203
3204 parsed_footers.append(('Cr-Commit-Position',
3205 '%s@{#%d}' % (dest_ref, number)))
3206 parsed_footers.extend(('Cr-Branched-From', v) for v in lineage)
3207
3208 self._description_lines = top_lines
3209 if cp_line:
3210 self._description_lines.append(cp_line)
3211 if self._description_lines[-1] != '':
3212 self._description_lines.append('') # Ensure footer separator.
3213 self._description_lines.extend('%s: %s' % kv for kv in parsed_footers)
3214
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003215
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003216def get_approving_reviewers(props):
3217 """Retrieves the reviewers that approved a CL from the issue properties with
3218 messages.
3219
3220 Note that the list may contain reviewers that are not committer, thus are not
3221 considered by the CQ.
3222 """
3223 return sorted(
3224 set(
3225 message['sender']
3226 for message in props['messages']
3227 if message['approval'] and message['sender'] in props['reviewers']
3228 )
3229 )
3230
3231
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003232def FindCodereviewSettingsFile(filename='codereview.settings'):
3233 """Finds the given file starting in the cwd and going up.
3234
3235 Only looks up to the top of the repository unless an
3236 'inherit-review-settings-ok' file exists in the root of the repository.
3237 """
3238 inherit_ok_file = 'inherit-review-settings-ok'
3239 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003240 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003241 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3242 root = '/'
3243 while True:
3244 if filename in os.listdir(cwd):
3245 if os.path.isfile(os.path.join(cwd, filename)):
3246 return open(os.path.join(cwd, filename))
3247 if cwd == root:
3248 break
3249 cwd = os.path.dirname(cwd)
3250
3251
3252def LoadCodereviewSettingsFromFile(fileobj):
3253 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003254 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003255
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003256 def SetProperty(name, setting, unset_error_ok=False):
3257 fullname = 'rietveld.' + name
3258 if setting in keyvals:
3259 RunGit(['config', fullname, keyvals[setting]])
3260 else:
3261 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3262
tandrii48df5812016-10-17 03:55:37 -07003263 if not keyvals.get('GERRIT_HOST', False):
3264 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003265 # Only server setting is required. Other settings can be absent.
3266 # In that case, we ignore errors raised during option deletion attempt.
3267 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003268 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003269 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3270 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003271 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003272 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003273 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
3274 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003275 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003276 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003277 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003278 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3279 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003280
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003281 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003282 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003283
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003284 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003285 RunGit(['config', 'gerrit.squash-uploads',
3286 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003287
tandrii@chromium.org28253532016-04-14 13:46:56 +00003288 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003289 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003290 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3291
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003292 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3293 #should be of the form
3294 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3295 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
3296 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3297 keyvals['ORIGIN_URL_CONFIG']])
3298
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003299
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003300def urlretrieve(source, destination):
3301 """urllib is broken for SSL connections via a proxy therefore we
3302 can't use urllib.urlretrieve()."""
3303 with open(destination, 'w') as f:
3304 f.write(urllib2.urlopen(source).read())
3305
3306
ukai@chromium.org712d6102013-11-27 00:52:58 +00003307def hasSheBang(fname):
3308 """Checks fname is a #! script."""
3309 with open(fname) as f:
3310 return f.read(2).startswith('#!')
3311
3312
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003313# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3314def DownloadHooks(*args, **kwargs):
3315 pass
3316
3317
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003318def DownloadGerritHook(force):
3319 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003320
3321 Args:
3322 force: True to update hooks. False to install hooks if not present.
3323 """
3324 if not settings.GetIsGerrit():
3325 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003326 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003327 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3328 if not os.access(dst, os.X_OK):
3329 if os.path.exists(dst):
3330 if not force:
3331 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003332 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003333 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003334 if not hasSheBang(dst):
3335 DieWithError('Not a script: %s\n'
3336 'You need to download from\n%s\n'
3337 'into .git/hooks/commit-msg and '
3338 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003339 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3340 except Exception:
3341 if os.path.exists(dst):
3342 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003343 DieWithError('\nFailed to download hooks.\n'
3344 'You need to download from\n%s\n'
3345 'into .git/hooks/commit-msg and '
3346 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003347
3348
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003349
3350def GetRietveldCodereviewSettingsInteractively():
3351 """Prompt the user for settings."""
3352 server = settings.GetDefaultServerUrl(error_ok=True)
3353 prompt = 'Rietveld server (host[:port])'
3354 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3355 newserver = ask_for_data(prompt + ':')
3356 if not server and not newserver:
3357 newserver = DEFAULT_SERVER
3358 if newserver:
3359 newserver = gclient_utils.UpgradeToHttps(newserver)
3360 if newserver != server:
3361 RunGit(['config', 'rietveld.server', newserver])
3362
3363 def SetProperty(initial, caption, name, is_url):
3364 prompt = caption
3365 if initial:
3366 prompt += ' ("x" to clear) [%s]' % initial
3367 new_val = ask_for_data(prompt + ':')
3368 if new_val == 'x':
3369 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3370 elif new_val:
3371 if is_url:
3372 new_val = gclient_utils.UpgradeToHttps(new_val)
3373 if new_val != initial:
3374 RunGit(['config', 'rietveld.' + name, new_val])
3375
3376 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3377 SetProperty(settings.GetDefaultPrivateFlag(),
3378 'Private flag (rietveld only)', 'private', False)
3379 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3380 'tree-status-url', False)
3381 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3382 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3383 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3384 'run-post-upload-hook', False)
3385
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003386@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003387def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003388 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003389
tandrii5d0a0422016-09-14 06:24:35 -07003390 print('WARNING: git cl config works for Rietveld only')
3391 # TODO(tandrii): remove this once we switch to Gerrit.
3392 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003393 parser.add_option('--activate-update', action='store_true',
3394 help='activate auto-updating [rietveld] section in '
3395 '.git/config')
3396 parser.add_option('--deactivate-update', action='store_true',
3397 help='deactivate auto-updating [rietveld] section in '
3398 '.git/config')
3399 options, args = parser.parse_args(args)
3400
3401 if options.deactivate_update:
3402 RunGit(['config', 'rietveld.autoupdate', 'false'])
3403 return
3404
3405 if options.activate_update:
3406 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3407 return
3408
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003409 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003410 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003411 return 0
3412
3413 url = args[0]
3414 if not url.endswith('codereview.settings'):
3415 url = os.path.join(url, 'codereview.settings')
3416
3417 # Load code review settings and download hooks (if available).
3418 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3419 return 0
3420
3421
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003422def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003423 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003424 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3425 branch = ShortBranchName(branchref)
3426 _, args = parser.parse_args(args)
3427 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003428 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003429 return RunGit(['config', 'branch.%s.base-url' % branch],
3430 error_ok=False).strip()
3431 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003432 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003433 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3434 error_ok=False).strip()
3435
3436
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003437def color_for_status(status):
3438 """Maps a Changelist status to color, for CMDstatus and other tools."""
3439 return {
3440 'unsent': Fore.RED,
3441 'waiting': Fore.BLUE,
3442 'reply': Fore.YELLOW,
3443 'lgtm': Fore.GREEN,
3444 'commit': Fore.MAGENTA,
3445 'closed': Fore.CYAN,
3446 'error': Fore.WHITE,
3447 }.get(status, Fore.WHITE)
3448
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003449
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003450def get_cl_statuses(changes, fine_grained, max_processes=None):
3451 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003452
3453 If fine_grained is true, this will fetch CL statuses from the server.
3454 Otherwise, simply indicate if there's a matching url for the given branches.
3455
3456 If max_processes is specified, it is used as the maximum number of processes
3457 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3458 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003459
3460 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003461 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003462 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003463 upload.verbosity = 0
3464
3465 if fine_grained:
3466 # Process one branch synchronously to work through authentication, then
3467 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003468 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003469 def fetch(cl):
3470 try:
3471 return (cl, cl.GetStatus())
3472 except:
3473 # See http://crbug.com/629863.
3474 logging.exception('failed to fetch status for %s:', cl)
3475 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003476 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003477
tandriiea9514a2016-08-17 12:32:37 -07003478 changes_to_fetch = changes[1:]
3479 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003480 # Exit early if there was only one branch to fetch.
3481 return
3482
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003483 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003484 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003485 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003486 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003487
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003488 fetched_cls = set()
3489 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003490 while True:
3491 try:
3492 row = it.next(timeout=5)
3493 except multiprocessing.TimeoutError:
3494 break
3495
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003496 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003497 yield row
3498
3499 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003500 for cl in set(changes_to_fetch) - fetched_cls:
3501 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003502
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003503 else:
3504 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003505 for cl in changes:
3506 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003507
rmistry@google.com2dd99862015-06-22 12:22:18 +00003508
3509def upload_branch_deps(cl, args):
3510 """Uploads CLs of local branches that are dependents of the current branch.
3511
3512 If the local branch dependency tree looks like:
3513 test1 -> test2.1 -> test3.1
3514 -> test3.2
3515 -> test2.2 -> test3.3
3516
3517 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3518 run on the dependent branches in this order:
3519 test2.1, test3.1, test3.2, test2.2, test3.3
3520
3521 Note: This function does not rebase your local dependent branches. Use it when
3522 you make a change to the parent branch that will not conflict with its
3523 dependent branches, and you would like their dependencies updated in
3524 Rietveld.
3525 """
3526 if git_common.is_dirty_git_tree('upload-branch-deps'):
3527 return 1
3528
3529 root_branch = cl.GetBranch()
3530 if root_branch is None:
3531 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3532 'Get on a branch!')
3533 if not cl.GetIssue() or not cl.GetPatchset():
3534 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3535 'patchset dependencies without an uploaded CL.')
3536
3537 branches = RunGit(['for-each-ref',
3538 '--format=%(refname:short) %(upstream:short)',
3539 'refs/heads'])
3540 if not branches:
3541 print('No local branches found.')
3542 return 0
3543
3544 # Create a dictionary of all local branches to the branches that are dependent
3545 # on it.
3546 tracked_to_dependents = collections.defaultdict(list)
3547 for b in branches.splitlines():
3548 tokens = b.split()
3549 if len(tokens) == 2:
3550 branch_name, tracked = tokens
3551 tracked_to_dependents[tracked].append(branch_name)
3552
vapiera7fbd5a2016-06-16 09:17:49 -07003553 print()
3554 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003555 dependents = []
3556 def traverse_dependents_preorder(branch, padding=''):
3557 dependents_to_process = tracked_to_dependents.get(branch, [])
3558 padding += ' '
3559 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003560 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003561 dependents.append(dependent)
3562 traverse_dependents_preorder(dependent, padding)
3563 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003564 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003565
3566 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003567 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003568 return 0
3569
vapiera7fbd5a2016-06-16 09:17:49 -07003570 print('This command will checkout all dependent branches and run '
3571 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003572 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3573
andybons@chromium.org962f9462016-02-03 20:00:42 +00003574 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003575 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003576 args.extend(['-t', 'Updated patchset dependency'])
3577
rmistry@google.com2dd99862015-06-22 12:22:18 +00003578 # Record all dependents that failed to upload.
3579 failures = {}
3580 # Go through all dependents, checkout the branch and upload.
3581 try:
3582 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003583 print()
3584 print('--------------------------------------')
3585 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003586 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003587 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003588 try:
3589 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003590 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003591 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003592 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003593 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003594 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003595 finally:
3596 # Swap back to the original root branch.
3597 RunGit(['checkout', '-q', root_branch])
3598
vapiera7fbd5a2016-06-16 09:17:49 -07003599 print()
3600 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003601 for dependent_branch in dependents:
3602 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003603 print(' %s : %s' % (dependent_branch, upload_status))
3604 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003605
3606 return 0
3607
3608
kmarshall3bff56b2016-06-06 18:31:47 -07003609def CMDarchive(parser, args):
3610 """Archives and deletes branches associated with closed changelists."""
3611 parser.add_option(
3612 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003613 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003614 parser.add_option(
3615 '-f', '--force', action='store_true',
3616 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003617 parser.add_option(
3618 '-d', '--dry-run', action='store_true',
3619 help='Skip the branch tagging and removal steps.')
3620 parser.add_option(
3621 '-t', '--notags', action='store_true',
3622 help='Do not tag archived branches. '
3623 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003624
3625 auth.add_auth_options(parser)
3626 options, args = parser.parse_args(args)
3627 if args:
3628 parser.error('Unsupported args: %s' % ' '.join(args))
3629 auth_config = auth.extract_auth_config_from_options(options)
3630
3631 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3632 if not branches:
3633 return 0
3634
vapiera7fbd5a2016-06-16 09:17:49 -07003635 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003636 changes = [Changelist(branchref=b, auth_config=auth_config)
3637 for b in branches.splitlines()]
3638 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3639 statuses = get_cl_statuses(changes,
3640 fine_grained=True,
3641 max_processes=options.maxjobs)
3642 proposal = [(cl.GetBranch(),
3643 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3644 for cl, status in statuses
3645 if status == 'closed']
3646 proposal.sort()
3647
3648 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003649 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003650 return 0
3651
3652 current_branch = GetCurrentBranch()
3653
vapiera7fbd5a2016-06-16 09:17:49 -07003654 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003655 if options.notags:
3656 for next_item in proposal:
3657 print(' ' + next_item[0])
3658 else:
3659 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3660 for next_item in proposal:
3661 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003662
kmarshall9249e012016-08-23 12:02:16 -07003663 # Quit now on precondition failure or if instructed by the user, either
3664 # via an interactive prompt or by command line flags.
3665 if options.dry_run:
3666 print('\nNo changes were made (dry run).\n')
3667 return 0
3668 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003669 print('You are currently on a branch \'%s\' which is associated with a '
3670 'closed codereview issue, so archive cannot proceed. Please '
3671 'checkout another branch and run this command again.' %
3672 current_branch)
3673 return 1
kmarshall9249e012016-08-23 12:02:16 -07003674 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003675 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3676 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003677 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003678 return 1
3679
3680 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003681 if not options.notags:
3682 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003683 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003684
vapiera7fbd5a2016-06-16 09:17:49 -07003685 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003686
3687 return 0
3688
3689
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003690def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003691 """Show status of changelists.
3692
3693 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003694 - Red not sent for review or broken
3695 - Blue waiting for review
3696 - Yellow waiting for you to reply to review
3697 - Green LGTM'ed
3698 - Magenta in the commit queue
3699 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003700
3701 Also see 'git cl comments'.
3702 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003703 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003704 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003705 parser.add_option('-f', '--fast', action='store_true',
3706 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003707 parser.add_option(
3708 '-j', '--maxjobs', action='store', type=int,
3709 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003710
3711 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003712 _add_codereview_issue_select_options(
3713 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003714 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003715 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003716 if args:
3717 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003718 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003719
iannuccie53c9352016-08-17 14:40:40 -07003720 if options.issue is not None and not options.field:
3721 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003722
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003723 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003724 cl = Changelist(auth_config=auth_config, issue=options.issue,
3725 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003726 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003727 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003728 elif options.field == 'id':
3729 issueid = cl.GetIssue()
3730 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003731 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003732 elif options.field == 'patch':
3733 patchset = cl.GetPatchset()
3734 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003735 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003736 elif options.field == 'status':
3737 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003738 elif options.field == 'url':
3739 url = cl.GetIssueURL()
3740 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003741 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003742 return 0
3743
3744 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3745 if not branches:
3746 print('No local branch found.')
3747 return 0
3748
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003749 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003750 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003751 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003752 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003753 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003754 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003755 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003756
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003757 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003758 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3759 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3760 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003761 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003762 c, status = output.next()
3763 branch_statuses[c.GetBranch()] = status
3764 status = branch_statuses.pop(branch)
3765 url = cl.GetIssueURL()
3766 if url and (not status or status == 'error'):
3767 # The issue probably doesn't exist anymore.
3768 url += ' (broken)'
3769
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003770 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003771 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003772 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003773 color = ''
3774 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003775 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003776 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003777 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003778 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003779
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003780 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003781 print()
3782 print('Current branch:',)
3783 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003784 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003785 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003786 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003787 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003788 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003789 print('Issue description:')
3790 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003791 return 0
3792
3793
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003794def colorize_CMDstatus_doc():
3795 """To be called once in main() to add colors to git cl status help."""
3796 colors = [i for i in dir(Fore) if i[0].isupper()]
3797
3798 def colorize_line(line):
3799 for color in colors:
3800 if color in line.upper():
3801 # Extract whitespaces first and the leading '-'.
3802 indent = len(line) - len(line.lstrip(' ')) + 1
3803 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3804 return line
3805
3806 lines = CMDstatus.__doc__.splitlines()
3807 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3808
3809
phajdan.jre328cf92016-08-22 04:12:17 -07003810def write_json(path, contents):
3811 with open(path, 'w') as f:
3812 json.dump(contents, f)
3813
3814
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003815@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003816def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003817 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003818
3819 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003820 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003821 parser.add_option('-r', '--reverse', action='store_true',
3822 help='Lookup the branch(es) for the specified issues. If '
3823 'no issues are specified, all branches with mapped '
3824 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003825 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003826 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003827 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003828 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003829
dnj@chromium.org406c4402015-03-03 17:22:28 +00003830 if options.reverse:
3831 branches = RunGit(['for-each-ref', 'refs/heads',
3832 '--format=%(refname:short)']).splitlines()
3833
3834 # Reverse issue lookup.
3835 issue_branch_map = {}
3836 for branch in branches:
3837 cl = Changelist(branchref=branch)
3838 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3839 if not args:
3840 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003841 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003842 for issue in args:
3843 if not issue:
3844 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003845 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003846 print('Branch for issue number %s: %s' % (
3847 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003848 if options.json:
3849 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003850 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003851 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003852 if len(args) > 0:
3853 try:
3854 issue = int(args[0])
3855 except ValueError:
3856 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003857 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003858 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003859 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003860 if options.json:
3861 write_json(options.json, {
3862 'issue': cl.GetIssue(),
3863 'issue_url': cl.GetIssueURL(),
3864 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003865 return 0
3866
3867
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003868def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003869 """Shows or posts review comments for any changelist."""
3870 parser.add_option('-a', '--add-comment', dest='comment',
3871 help='comment to add to an issue')
3872 parser.add_option('-i', dest='issue',
3873 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003874 parser.add_option('-j', '--json-file',
3875 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003876 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003877 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003878 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003879
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003880 issue = None
3881 if options.issue:
3882 try:
3883 issue = int(options.issue)
3884 except ValueError:
3885 DieWithError('A review issue id is expected to be a number')
3886
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003887 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003888
3889 if options.comment:
3890 cl.AddComment(options.comment)
3891 return 0
3892
3893 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003894 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003895 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003896 summary.append({
3897 'date': message['date'],
3898 'lgtm': False,
3899 'message': message['text'],
3900 'not_lgtm': False,
3901 'sender': message['sender'],
3902 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003903 if message['disapproval']:
3904 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003905 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003906 elif message['approval']:
3907 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003908 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003909 elif message['sender'] == data['owner_email']:
3910 color = Fore.MAGENTA
3911 else:
3912 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003913 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003914 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003915 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003916 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003917 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003918 if options.json_file:
3919 with open(options.json_file, 'wb') as f:
3920 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003921 return 0
3922
3923
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003924@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003925def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003926 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003927 parser.add_option('-d', '--display', action='store_true',
3928 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003929 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003930 help='New description to set for this issue (- for stdin, '
3931 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003932 parser.add_option('-f', '--force', action='store_true',
3933 help='Delete any unpublished Gerrit edits for this issue '
3934 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003935
3936 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003937 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003938 options, args = parser.parse_args(args)
3939 _process_codereview_select_options(parser, options)
3940
3941 target_issue = None
3942 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003943 target_issue = ParseIssueNumberArgument(args[0])
3944 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003945 parser.print_help()
3946 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003947
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003948 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003949
martiniss6eda05f2016-06-30 10:18:35 -07003950 kwargs = {
3951 'auth_config': auth_config,
3952 'codereview': options.forced_codereview,
3953 }
3954 if target_issue:
3955 kwargs['issue'] = target_issue.issue
3956 if options.forced_codereview == 'rietveld':
3957 kwargs['rietveld_server'] = target_issue.hostname
3958
3959 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003960
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003961 if not cl.GetIssue():
3962 DieWithError('This branch has no associated changelist.')
3963 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003964
smut@google.com34fb6b12015-07-13 20:03:26 +00003965 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003966 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003967 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003968
3969 if options.new_description:
3970 text = options.new_description
3971 if text == '-':
3972 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003973 elif text == '+':
3974 base_branch = cl.GetCommonAncestorWithUpstream()
3975 change = cl.GetChange(base_branch, None, local_description=True)
3976 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003977
3978 description.set_description(text)
3979 else:
3980 description.prompt()
3981
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003982 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003983 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003984 return 0
3985
3986
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003987def CreateDescriptionFromLog(args):
3988 """Pulls out the commit log to use as a base for the CL description."""
3989 log_args = []
3990 if len(args) == 1 and not args[0].endswith('.'):
3991 log_args = [args[0] + '..']
3992 elif len(args) == 1 and args[0].endswith('...'):
3993 log_args = [args[0][:-1]]
3994 elif len(args) == 2:
3995 log_args = [args[0] + '..' + args[1]]
3996 else:
3997 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003998 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003999
4000
thestig@chromium.org44202a22014-03-11 19:22:18 +00004001def CMDlint(parser, args):
4002 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004003 parser.add_option('--filter', action='append', metavar='-x,+y',
4004 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004005 auth.add_auth_options(parser)
4006 options, args = parser.parse_args(args)
4007 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004008
4009 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004010 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004011 try:
4012 import cpplint
4013 import cpplint_chromium
4014 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004015 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004016 return 1
4017
4018 # Change the current working directory before calling lint so that it
4019 # shows the correct base.
4020 previous_cwd = os.getcwd()
4021 os.chdir(settings.GetRoot())
4022 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004023 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004024 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4025 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004026 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004027 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004028 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004029
4030 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004031 command = args + files
4032 if options.filter:
4033 command = ['--filter=' + ','.join(options.filter)] + command
4034 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004035
4036 white_regex = re.compile(settings.GetLintRegex())
4037 black_regex = re.compile(settings.GetLintIgnoreRegex())
4038 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4039 for filename in filenames:
4040 if white_regex.match(filename):
4041 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004042 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004043 else:
4044 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4045 extra_check_functions)
4046 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004047 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004048 finally:
4049 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004050 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004051 if cpplint._cpplint_state.error_count != 0:
4052 return 1
4053 return 0
4054
4055
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004056def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004057 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004058 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004059 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004060 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004061 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004062 auth.add_auth_options(parser)
4063 options, args = parser.parse_args(args)
4064 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004065
sbc@chromium.org71437c02015-04-09 19:29:40 +00004066 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004067 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004068 return 1
4069
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004070 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004071 if args:
4072 base_branch = args[0]
4073 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004074 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004075 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004076
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004077 cl.RunHook(
4078 committing=not options.upload,
4079 may_prompt=False,
4080 verbose=options.verbose,
4081 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004082 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004083
4084
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004085def GenerateGerritChangeId(message):
4086 """Returns Ixxxxxx...xxx change id.
4087
4088 Works the same way as
4089 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4090 but can be called on demand on all platforms.
4091
4092 The basic idea is to generate git hash of a state of the tree, original commit
4093 message, author/committer info and timestamps.
4094 """
4095 lines = []
4096 tree_hash = RunGitSilent(['write-tree'])
4097 lines.append('tree %s' % tree_hash.strip())
4098 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4099 if code == 0:
4100 lines.append('parent %s' % parent.strip())
4101 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4102 lines.append('author %s' % author.strip())
4103 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4104 lines.append('committer %s' % committer.strip())
4105 lines.append('')
4106 # Note: Gerrit's commit-hook actually cleans message of some lines and
4107 # whitespace. This code is not doing this, but it clearly won't decrease
4108 # entropy.
4109 lines.append(message)
4110 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4111 stdin='\n'.join(lines))
4112 return 'I%s' % change_hash.strip()
4113
4114
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004115def GetTargetRef(remote, remote_branch, target_branch, pending_prefix_check,
4116 remote_url=None):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004117 """Computes the remote branch ref to use for the CL.
4118
4119 Args:
4120 remote (str): The git remote for the CL.
4121 remote_branch (str): The git remote branch for the CL.
4122 target_branch (str): The target branch specified by the user.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004123 pending_prefix_check (bool): If true, determines if pending_prefix should be
4124 used.
4125 remote_url (str): Only used for checking if pending_prefix should be used.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004126 """
4127 if not (remote and remote_branch):
4128 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004129
wittman@chromium.org455dc922015-01-26 20:15:50 +00004130 if target_branch:
4131 # Cannonicalize branch references to the equivalent local full symbolic
4132 # refs, which are then translated into the remote full symbolic refs
4133 # below.
4134 if '/' not in target_branch:
4135 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4136 else:
4137 prefix_replacements = (
4138 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4139 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4140 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4141 )
4142 match = None
4143 for regex, replacement in prefix_replacements:
4144 match = re.search(regex, target_branch)
4145 if match:
4146 remote_branch = target_branch.replace(match.group(0), replacement)
4147 break
4148 if not match:
4149 # This is a branch path but not one we recognize; use as-is.
4150 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004151 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4152 # Handle the refs that need to land in different refs.
4153 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004154
wittman@chromium.org455dc922015-01-26 20:15:50 +00004155 # Create the true path to the remote branch.
4156 # Does the following translation:
4157 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4158 # * refs/remotes/origin/master -> refs/heads/master
4159 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4160 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4161 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4162 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4163 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4164 'refs/heads/')
4165 elif remote_branch.startswith('refs/remotes/branch-heads'):
4166 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004167
4168 if pending_prefix_check:
4169 # If a pending prefix exists then replace refs/ with it.
4170 state = _GitNumbererState.load(remote_url, remote_branch)
4171 if state.pending_prefix:
4172 remote_branch = remote_branch.replace('refs/', state.pending_prefix)
wittman@chromium.org455dc922015-01-26 20:15:50 +00004173 return remote_branch
4174
4175
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004176def cleanup_list(l):
4177 """Fixes a list so that comma separated items are put as individual items.
4178
4179 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4180 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4181 """
4182 items = sum((i.split(',') for i in l), [])
4183 stripped_items = (i.strip() for i in items)
4184 return sorted(filter(None, stripped_items))
4185
4186
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004187@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004188def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004189 """Uploads the current changelist to codereview.
4190
4191 Can skip dependency patchset uploads for a branch by running:
4192 git config branch.branch_name.skip-deps-uploads True
4193 To unset run:
4194 git config --unset branch.branch_name.skip-deps-uploads
4195 Can also set the above globally by using the --global flag.
4196 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004197 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4198 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004199 parser.add_option('--bypass-watchlists', action='store_true',
4200 dest='bypass_watchlists',
4201 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004202 parser.add_option('-f', action='store_true', dest='force',
4203 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004204 parser.add_option('--message', '-m', dest='message',
4205 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004206 parser.add_option('-b', '--bug',
4207 help='pre-populate the bug number(s) for this issue. '
4208 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004209 parser.add_option('--message-file', dest='message_file',
4210 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004211 parser.add_option('--title', '-t', dest='title',
4212 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004213 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004214 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004215 help='reviewer email addresses')
4216 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004217 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004218 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004219 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004220 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004221 parser.add_option('--emulate_svn_auto_props',
4222 '--emulate-svn-auto-props',
4223 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004224 dest="emulate_svn_auto_props",
4225 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004226 parser.add_option('-c', '--use-commit-queue', action='store_true',
4227 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004228 parser.add_option('--private', action='store_true',
4229 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004230 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004231 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004232 metavar='TARGET',
4233 help='Apply CL to remote ref TARGET. ' +
4234 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004235 parser.add_option('--squash', action='store_true',
4236 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004237 parser.add_option('--no-squash', action='store_true',
4238 help='Don\'t squash multiple commits into one ' +
4239 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004240 parser.add_option('--topic', default=None,
4241 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004242 parser.add_option('--email', default=None,
4243 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004244 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4245 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004246 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4247 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004248 help='Send the patchset to do a CQ dry run right after '
4249 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004250 parser.add_option('--dependencies', action='store_true',
4251 help='Uploads CLs of all the local branches that depend on '
4252 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004253
rmistry@google.com2dd99862015-06-22 12:22:18 +00004254 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004255 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004256 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004257 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004258 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004259 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004260 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004261
sbc@chromium.org71437c02015-04-09 19:29:40 +00004262 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004263 return 1
4264
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004265 options.reviewers = cleanup_list(options.reviewers)
4266 options.cc = cleanup_list(options.cc)
4267
tandriib80458a2016-06-23 12:20:07 -07004268 if options.message_file:
4269 if options.message:
4270 parser.error('only one of --message and --message-file allowed.')
4271 options.message = gclient_utils.FileRead(options.message_file)
4272 options.message_file = None
4273
tandrii4d0545a2016-07-06 03:56:49 -07004274 if options.cq_dry_run and options.use_commit_queue:
4275 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4276
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004277 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4278 settings.GetIsGerrit()
4279
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004280 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004281 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004282
4283
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004284def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004285 print()
4286 print('Waiting for commit to be landed on %s...' % real_ref)
4287 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004288 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4289 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004290 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004291
4292 loop = 0
4293 while True:
4294 sys.stdout.write('fetching (%d)... \r' % loop)
4295 sys.stdout.flush()
4296 loop += 1
4297
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004298 if mirror:
4299 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004300 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4301 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4302 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4303 for commit in commits.splitlines():
4304 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004305 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004306 return commit
4307
4308 current_rev = to_rev
4309
4310
tandriibf429402016-09-14 07:09:12 -07004311def PushToGitPending(remote, pending_ref):
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004312 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4313
4314 Returns:
4315 (retcode of last operation, output log of last operation).
4316 """
4317 assert pending_ref.startswith('refs/'), pending_ref
4318 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4319 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4320 code = 0
4321 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004322 max_attempts = 3
4323 attempts_left = max_attempts
4324 while attempts_left:
4325 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004326 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004327 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004328
4329 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004330 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004331 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004332 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004333 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004334 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004335 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004336 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004337 continue
4338
4339 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004340 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004341 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004342 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004343 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004344 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4345 'the following files have merge conflicts:' % pending_ref)
4346 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4347 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004348 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004349 return code, out
4350
4351 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004352 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004353 code, out = RunGitWithCode(
4354 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4355 if code == 0:
4356 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004357 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004358 return code, out
4359
vapiera7fbd5a2016-06-16 09:17:49 -07004360 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004361 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004362 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004363 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004364 print('Fatal push error. Make sure your .netrc credentials and git '
4365 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004366 return code, out
4367
vapiera7fbd5a2016-06-16 09:17:49 -07004368 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004369 return code, out
4370
4371
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004372def IsFatalPushFailure(push_stdout):
4373 """True if retrying push won't help."""
4374 return '(prohibited by Gerrit)' in push_stdout
4375
4376
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004377@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004378def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004379 """DEPRECATED: Used to commit the current changelist via git-svn."""
4380 message = ('git-cl no longer supports committing to SVN repositories via '
4381 'git-svn. You probably want to use `git cl land` instead.')
4382 print(message)
4383 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004384
4385
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004386@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004387def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004388 """Commits the current changelist via git.
4389
4390 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4391 upstream and closes the issue automatically and atomically.
4392
4393 Otherwise (in case of Rietveld):
4394 Squashes branch into a single commit.
4395 Updates commit message with metadata (e.g. pointer to review).
4396 Pushes the code upstream.
4397 Updates review and closes.
4398 """
4399 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4400 help='bypass upload presubmit hook')
4401 parser.add_option('-m', dest='message',
4402 help="override review description")
4403 parser.add_option('-f', action='store_true', dest='force',
4404 help="force yes to questions (don't prompt)")
4405 parser.add_option('-c', dest='contributor',
4406 help="external contributor for patch (appended to " +
4407 "description and used as author for git). Should be " +
4408 "formatted as 'First Last <email@example.com>'")
4409 add_git_similarity(parser)
4410 auth.add_auth_options(parser)
4411 (options, args) = parser.parse_args(args)
4412 auth_config = auth.extract_auth_config_from_options(options)
4413
4414 cl = Changelist(auth_config=auth_config)
4415
4416 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4417 if cl.IsGerrit():
4418 if options.message:
4419 # This could be implemented, but it requires sending a new patch to
4420 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4421 # Besides, Gerrit has the ability to change the commit message on submit
4422 # automatically, thus there is no need to support this option (so far?).
4423 parser.error('-m MESSAGE option is not supported for Gerrit.')
4424 if options.contributor:
4425 parser.error(
4426 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4427 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4428 'the contributor\'s "name <email>". If you can\'t upload such a '
4429 'commit for review, contact your repository admin and request'
4430 '"Forge-Author" permission.')
4431 if not cl.GetIssue():
4432 DieWithError('You must upload the change first to Gerrit.\n'
4433 ' If you would rather have `git cl land` upload '
4434 'automatically for you, see http://crbug.com/642759')
4435 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4436 options.verbose)
4437
4438 current = cl.GetBranch()
4439 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4440 if remote == '.':
4441 print()
4442 print('Attempting to push branch %r into another local branch!' % current)
4443 print()
4444 print('Either reparent this branch on top of origin/master:')
4445 print(' git reparent-branch --root')
4446 print()
4447 print('OR run `git rebase-update` if you think the parent branch is ')
4448 print('already committed.')
4449 print()
4450 print(' Current parent: %r' % upstream_branch)
4451 return 1
4452
4453 if not args:
4454 # Default to merging against our best guess of the upstream branch.
4455 args = [cl.GetUpstreamBranch()]
4456
4457 if options.contributor:
4458 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
4459 print("Please provide contibutor as 'First Last <email@example.com>'")
4460 return 1
4461
4462 base_branch = args[0]
4463
4464 if git_common.is_dirty_git_tree('land'):
4465 return 1
4466
4467 # This rev-list syntax means "show all commits not in my branch that
4468 # are in base_branch".
4469 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4470 base_branch]).splitlines()
4471 if upstream_commits:
4472 print('Base branch "%s" has %d commits '
4473 'not in this branch.' % (base_branch, len(upstream_commits)))
4474 print('Run "git merge %s" before attempting to land.' % base_branch)
4475 return 1
4476
4477 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
4478 if not options.bypass_hooks:
4479 author = None
4480 if options.contributor:
4481 author = re.search(r'\<(.*)\>', options.contributor).group(1)
4482 hook_results = cl.RunHook(
4483 committing=True,
4484 may_prompt=not options.force,
4485 verbose=options.verbose,
4486 change=cl.GetChange(merge_base, author))
4487 if not hook_results.should_continue():
4488 return 1
4489
4490 # Check the tree status if the tree status URL is set.
4491 status = GetTreeStatus()
4492 if 'closed' == status:
4493 print('The tree is closed. Please wait for it to reopen. Use '
4494 '"git cl land --bypass-hooks" to commit on a closed tree.')
4495 return 1
4496 elif 'unknown' == status:
4497 print('Unable to determine tree status. Please verify manually and '
4498 'use "git cl land --bypass-hooks" to commit on a closed tree.')
4499 return 1
4500
4501 change_desc = ChangeDescription(options.message)
4502 if not change_desc.description and cl.GetIssue():
4503 change_desc = ChangeDescription(cl.GetDescription())
4504
4505 if not change_desc.description:
4506 if not cl.GetIssue() and options.bypass_hooks:
4507 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
4508 else:
4509 print('No description set.')
4510 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
4511 return 1
4512
4513 # Keep a separate copy for the commit message, because the commit message
4514 # contains the link to the Rietveld issue, while the Rietveld message contains
4515 # the commit viewvc url.
4516 if cl.GetIssue():
4517 change_desc.update_reviewers(cl.GetApprovingReviewers())
4518
4519 commit_desc = ChangeDescription(change_desc.description)
4520 if cl.GetIssue():
4521 # Xcode won't linkify this URL unless there is a non-whitespace character
4522 # after it. Add a period on a new line to circumvent this. Also add a space
4523 # before the period to make sure that Gitiles continues to correctly resolve
4524 # the URL.
4525 commit_desc.append_footer('Review-Url: %s .' % cl.GetIssueURL())
4526 if options.contributor:
4527 commit_desc.append_footer('Patch from %s.' % options.contributor)
4528
4529 print('Description:')
4530 print(commit_desc.description)
4531
4532 branches = [merge_base, cl.GetBranchRef()]
4533 if not options.force:
4534 print_stats(options.similarity, options.find_copies, branches)
4535
4536 # We want to squash all this branch's commits into one commit with the proper
4537 # description. We do this by doing a "reset --soft" to the base branch (which
4538 # keeps the working copy the same), then landing that.
4539 MERGE_BRANCH = 'git-cl-commit'
4540 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4541 # Delete the branches if they exist.
4542 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4543 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4544 result = RunGitWithCode(showref_cmd)
4545 if result[0] == 0:
4546 RunGit(['branch', '-D', branch])
4547
4548 # We might be in a directory that's present in this branch but not in the
4549 # trunk. Move up to the top of the tree so that git commands that expect a
4550 # valid CWD won't fail after we check out the merge branch.
4551 rel_base_path = settings.GetRelativeRoot()
4552 if rel_base_path:
4553 os.chdir(rel_base_path)
4554
4555 # Stuff our change into the merge branch.
4556 # We wrap in a try...finally block so if anything goes wrong,
4557 # we clean up the branches.
4558 retcode = -1
4559 pushed_to_pending = False
4560 pending_ref = None
4561 revision = None
4562 try:
4563 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
4564 RunGit(['reset', '--soft', merge_base])
4565 if options.contributor:
4566 RunGit(
4567 [
4568 'commit', '--author', options.contributor,
4569 '-m', commit_desc.description,
4570 ])
4571 else:
4572 RunGit(['commit', '-m', commit_desc.description])
4573
4574 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
4575 mirror = settings.GetGitMirror(remote)
4576 if mirror:
4577 pushurl = mirror.url
4578 git_numberer = _GitNumbererState.load(pushurl, branch)
4579 else:
4580 pushurl = remote # Usually, this is 'origin'.
4581 git_numberer = _GitNumbererState.load(
4582 RunGit(['config', 'remote.%s.url' % remote]).strip(), branch)
4583
4584 if git_numberer.should_add_git_number:
4585 # TODO(tandrii): run git fetch in a loop + autorebase when there there
4586 # is no pending ref to push to?
4587 logging.debug('Adding git number footers')
4588 parent_msg = RunGit(['show', '-s', '--format=%B', merge_base]).strip()
4589 commit_desc.update_with_git_number_footers(merge_base, parent_msg,
4590 branch)
4591 # Ensure timestamps are monotonically increasing.
4592 timestamp = max(1 + _get_committer_timestamp(merge_base),
4593 _get_committer_timestamp('HEAD'))
4594 _git_amend_head(commit_desc.description, timestamp)
4595 change_desc = ChangeDescription(commit_desc.description)
4596 # If gnumbd is sitll ON and we ultimately push to branch with
4597 # pending_prefix, gnumbd will modify footers we've just inserted with
4598 # 'Original-', which is annoying but still technically correct.
4599
4600 pending_prefix = git_numberer.pending_prefix
4601 if not pending_prefix or branch.startswith(pending_prefix):
4602 # If not using refs/pending/heads/* at all, or target ref is already set
4603 # to pending, then push to the target ref directly.
4604 # NB(tandrii): I think branch.startswith(pending_prefix) never happens
4605 # in practise. I really tried to create a new branch tracking
4606 # refs/pending/heads/master directly and git cl land failed long before
4607 # reaching this. Disagree? Comment on http://crbug.com/642493.
4608 if pending_prefix:
4609 print('\n\nYOU GOT A CHANCE TO WIN A FREE GIFT!\n\n'
4610 'Grab your .git/config, add instructions how to reproduce '
4611 'this, and post it to http://crbug.com/642493.\n'
4612 'The first reporter gets a free "Black Swan" book from '
4613 'tandrii@\n\n')
4614 retcode, output = RunGitWithCode(
4615 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
4616 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
4617 else:
4618 # Cherry-pick the change on top of pending ref and then push it.
4619 assert branch.startswith('refs/'), branch
4620 assert pending_prefix[-1] == '/', pending_prefix
4621 pending_ref = pending_prefix + branch[len('refs/'):]
4622 retcode, output = PushToGitPending(pushurl, pending_ref)
4623 pushed_to_pending = (retcode == 0)
4624
4625 if retcode == 0:
4626 revision = RunGit(['rev-parse', 'HEAD']).strip()
4627 logging.debug(output)
4628 except: # pylint: disable=bare-except
4629 if _IS_BEING_TESTED:
4630 logging.exception('this is likely your ACTUAL cause of test failure.\n'
4631 + '-' * 30 + '8<' + '-' * 30)
4632 logging.error('\n' + '-' * 30 + '8<' + '-' * 30 + '\n\n\n')
4633 raise
4634 finally:
4635 # And then swap back to the original branch and clean up.
4636 RunGit(['checkout', '-q', cl.GetBranch()])
4637 RunGit(['branch', '-D', MERGE_BRANCH])
4638
4639 if not revision:
4640 print('Failed to push. If this persists, please file a bug.')
4641 return 1
4642
4643 killed = False
4644 if pushed_to_pending:
4645 try:
4646 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4647 # We set pushed_to_pending to False, since it made it all the way to the
4648 # real ref.
4649 pushed_to_pending = False
4650 except KeyboardInterrupt:
4651 killed = True
4652
4653 if cl.GetIssue():
4654 to_pending = ' to pending queue' if pushed_to_pending else ''
4655 viewvc_url = settings.GetViewVCUrl()
4656 if not to_pending:
4657 if viewvc_url and revision:
4658 change_desc.append_footer(
4659 'Committed: %s%s' % (viewvc_url, revision))
4660 elif revision:
4661 change_desc.append_footer('Committed: %s' % (revision,))
4662 print('Closing issue '
4663 '(you may be prompted for your codereview password)...')
4664 cl.UpdateDescription(change_desc.description)
4665 cl.CloseIssue()
4666 props = cl.GetIssueProperties()
4667 patch_num = len(props['patchsets'])
4668 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
4669 patch_num, props['patchsets'][-1], to_pending, revision)
4670 if options.bypass_hooks:
4671 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4672 else:
4673 comment += ' (presubmit successful).'
4674 cl.RpcServer().add_comment(cl.GetIssue(), comment)
4675
4676 if pushed_to_pending:
4677 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
4678 print('The commit is in the pending queue (%s).' % pending_ref)
4679 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4680 'footer.' % branch)
4681
4682 if os.path.isfile(POSTUPSTREAM_HOOK):
4683 RunCommand([POSTUPSTREAM_HOOK, merge_base], error_ok=True)
4684
4685 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004686
4687
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004688@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004689def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004690 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004691 parser.add_option('-b', dest='newbranch',
4692 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004693 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004694 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004695 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4696 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004697 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004698 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004699 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004700 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004701 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004702 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004703
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004704
4705 group = optparse.OptionGroup(
4706 parser,
4707 'Options for continuing work on the current issue uploaded from a '
4708 'different clone (e.g. different machine). Must be used independently '
4709 'from the other options. No issue number should be specified, and the '
4710 'branch must have an issue number associated with it')
4711 group.add_option('--reapply', action='store_true', dest='reapply',
4712 help='Reset the branch and reapply the issue.\n'
4713 'CAUTION: This will undo any local changes in this '
4714 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004715
4716 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004717 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004718 parser.add_option_group(group)
4719
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004720 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004721 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004722 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004723 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004724 auth_config = auth.extract_auth_config_from_options(options)
4725
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004726
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004727 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004728 if options.newbranch:
4729 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004730 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004731 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004732
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004733 cl = Changelist(auth_config=auth_config,
4734 codereview=options.forced_codereview)
4735 if not cl.GetIssue():
4736 parser.error('current branch must have an associated issue')
4737
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004738 upstream = cl.GetUpstreamBranch()
4739 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004740 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004741
4742 RunGit(['reset', '--hard', upstream])
4743 if options.pull:
4744 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004745
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004746 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4747 options.directory)
4748
4749 if len(args) != 1 or not args[0]:
4750 parser.error('Must specify issue number or url')
4751
4752 # We don't want uncommitted changes mixed up with the patch.
4753 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004754 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004755
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004756 if options.newbranch:
4757 if options.force:
4758 RunGit(['branch', '-D', options.newbranch],
4759 stderr=subprocess2.PIPE, error_ok=True)
4760 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004761 elif not GetCurrentBranch():
4762 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004763
4764 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4765
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004766 if cl.IsGerrit():
4767 if options.reject:
4768 parser.error('--reject is not supported with Gerrit codereview.')
4769 if options.nocommit:
4770 parser.error('--nocommit is not supported with Gerrit codereview.')
4771 if options.directory:
4772 parser.error('--directory is not supported with Gerrit codereview.')
4773
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004774 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004775 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004776
4777
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004778def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004779 """Fetches the tree status and returns either 'open', 'closed',
4780 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004781 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004782 if url:
4783 status = urllib2.urlopen(url).read().lower()
4784 if status.find('closed') != -1 or status == '0':
4785 return 'closed'
4786 elif status.find('open') != -1 or status == '1':
4787 return 'open'
4788 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004789 return 'unset'
4790
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004791
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004792def GetTreeStatusReason():
4793 """Fetches the tree status from a json url and returns the message
4794 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004795 url = settings.GetTreeStatusUrl()
4796 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004797 connection = urllib2.urlopen(json_url)
4798 status = json.loads(connection.read())
4799 connection.close()
4800 return status['message']
4801
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004802
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004803def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004804 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004805 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004806 status = GetTreeStatus()
4807 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004808 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004809 return 2
4810
vapiera7fbd5a2016-06-16 09:17:49 -07004811 print('The tree is %s' % status)
4812 print()
4813 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004814 if status != 'open':
4815 return 1
4816 return 0
4817
4818
maruel@chromium.org15192402012-09-06 12:38:29 +00004819def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004820 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004821 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004822 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004823 '-b', '--bot', action='append',
4824 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4825 'times to specify multiple builders. ex: '
4826 '"-b win_rel -b win_layout". See '
4827 'the try server waterfall for the builders name and the tests '
4828 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004829 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004830 '-B', '--bucket', default='',
4831 help=('Buildbucket bucket to send the try requests.'))
4832 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004833 '-m', '--master', default='',
4834 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004835 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004836 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004837 help='Revision to use for the try job; default: the revision will '
4838 'be determined by the try recipe that builder runs, which usually '
4839 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004840 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004841 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004842 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004843 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004844 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004845 '--project',
4846 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004847 'in recipe to determine to which repository or directory to '
4848 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004849 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004850 '-p', '--property', dest='properties', action='append', default=[],
4851 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004852 'key2=value2 etc. The value will be treated as '
4853 'json if decodable, or as string otherwise. '
4854 'NOTE: using this may make your try job not usable for CQ, '
4855 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004856 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004857 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4858 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004859 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004860 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004861 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004862 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004863
machenbach@chromium.org45453142015-09-15 08:45:22 +00004864 # Make sure that all properties are prop=value pairs.
4865 bad_params = [x for x in options.properties if '=' not in x]
4866 if bad_params:
4867 parser.error('Got properties with missing "=": %s' % bad_params)
4868
maruel@chromium.org15192402012-09-06 12:38:29 +00004869 if args:
4870 parser.error('Unknown arguments: %s' % args)
4871
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004872 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004873 if not cl.GetIssue():
4874 parser.error('Need to upload first')
4875
tandriie113dfd2016-10-11 10:20:12 -07004876 error_message = cl.CannotTriggerTryJobReason()
4877 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004878 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004879
borenet6c0efe62016-10-19 08:13:29 -07004880 if options.bucket and options.master:
4881 parser.error('Only one of --bucket and --master may be used.')
4882
qyearsley1fdfcb62016-10-24 13:22:03 -07004883 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004884
qyearsleydd49f942016-10-28 11:57:22 -07004885 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4886 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004887 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004888 if options.verbose:
4889 print('git cl try with no bots now defaults to CQ Dry Run.')
4890 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004891
borenet6c0efe62016-10-19 08:13:29 -07004892 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004893 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004894 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004895 'of bot requires an initial job from a parent (usually a builder). '
4896 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004897 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004898 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004899
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004900 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004901 # TODO(tandrii): Checking local patchset against remote patchset is only
4902 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4903 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07004904 print('Warning: Codereview server has newer patchsets (%s) than most '
4905 'recent upload from local checkout (%s). Did a previous upload '
4906 'fail?\n'
4907 'By default, git cl try uses the latest patchset from '
4908 'codereview, continuing to use patchset %s.\n' %
4909 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07004910
tandrii568043b2016-10-11 07:49:18 -07004911 try:
borenet6c0efe62016-10-19 08:13:29 -07004912 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
4913 patchset)
tandrii568043b2016-10-11 07:49:18 -07004914 except BuildbucketResponseException as ex:
4915 print('ERROR: %s' % ex)
4916 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004917 return 0
4918
4919
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004920def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004921 """Prints info about try jobs associated with current CL."""
4922 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004923 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004924 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004925 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004926 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004927 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004928 '--color', action='store_true', default=setup_color.IS_TTY,
4929 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004930 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004931 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4932 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004933 group.add_option(
4934 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004935 parser.add_option_group(group)
4936 auth.add_auth_options(parser)
4937 options, args = parser.parse_args(args)
4938 if args:
4939 parser.error('Unrecognized args: %s' % ' '.join(args))
4940
4941 auth_config = auth.extract_auth_config_from_options(options)
4942 cl = Changelist(auth_config=auth_config)
4943 if not cl.GetIssue():
4944 parser.error('Need to upload first')
4945
tandrii221ab252016-10-06 08:12:04 -07004946 patchset = options.patchset
4947 if not patchset:
4948 patchset = cl.GetMostRecentPatchset()
4949 if not patchset:
4950 parser.error('Codereview doesn\'t know about issue %s. '
4951 'No access to issue or wrong issue number?\n'
4952 'Either upload first, or pass --patchset explicitely' %
4953 cl.GetIssue())
4954
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004955 # TODO(tandrii): Checking local patchset against remote patchset is only
4956 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4957 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07004958 print('Warning: Codereview server has newer patchsets (%s) than most '
4959 'recent upload from local checkout (%s). Did a previous upload '
4960 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07004961 'By default, git cl try-results uses the latest patchset from '
4962 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07004963 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004964 try:
tandrii221ab252016-10-06 08:12:04 -07004965 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004966 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004967 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004968 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004969 if options.json:
4970 write_try_results_json(options.json, jobs)
4971 else:
4972 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004973 return 0
4974
4975
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004976@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004977def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004978 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004979 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004980 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004981 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004982
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004983 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004984 if args:
4985 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004986 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004987 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004988 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004989 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004990
4991 # Clear configured merge-base, if there is one.
4992 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004993 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004994 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004995 return 0
4996
4997
thestig@chromium.org00858c82013-12-02 23:08:03 +00004998def CMDweb(parser, args):
4999 """Opens the current CL in the web browser."""
5000 _, args = parser.parse_args(args)
5001 if args:
5002 parser.error('Unrecognized args: %s' % ' '.join(args))
5003
5004 issue_url = Changelist().GetIssueURL()
5005 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005006 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005007 return 1
5008
5009 webbrowser.open(issue_url)
5010 return 0
5011
5012
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005013def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005014 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005015 parser.add_option('-d', '--dry-run', action='store_true',
5016 help='trigger in dry run mode')
5017 parser.add_option('-c', '--clear', action='store_true',
5018 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005019 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005020 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005021 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005022 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005023 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005024 if args:
5025 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005026 if options.dry_run and options.clear:
5027 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5028
iannuccie53c9352016-08-17 14:40:40 -07005029 cl = Changelist(auth_config=auth_config, issue=options.issue,
5030 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005031 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005032 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005033 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07005034 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005035 state = _CQState.DRY_RUN
5036 else:
5037 state = _CQState.COMMIT
5038 if not cl.GetIssue():
5039 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005040 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005041 return 0
5042
5043
groby@chromium.org411034a2013-02-26 15:12:01 +00005044def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005045 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005046 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005047 auth.add_auth_options(parser)
5048 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005049 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005050 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005051 if args:
5052 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005053 cl = Changelist(auth_config=auth_config, issue=options.issue,
5054 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005055 # Ensure there actually is an issue to close.
5056 cl.GetDescription()
5057 cl.CloseIssue()
5058 return 0
5059
5060
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005061def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005062 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005063 parser.add_option(
5064 '--stat',
5065 action='store_true',
5066 dest='stat',
5067 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005068 auth.add_auth_options(parser)
5069 options, args = parser.parse_args(args)
5070 auth_config = auth.extract_auth_config_from_options(options)
5071 if args:
5072 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005073
5074 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005075 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005076 # Staged changes would be committed along with the patch from last
5077 # upload, hence counted toward the "last upload" side in the final
5078 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005079 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005080 return 1
5081
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005082 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005083 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005084 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005085 if not issue:
5086 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005087 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005088 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005089
5090 # Create a new branch based on the merge-base
5091 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005092 # Clear cached branch in cl object, to avoid overwriting original CL branch
5093 # properties.
5094 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005095 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005096 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005097 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005098 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005099 return rtn
5100
wychen@chromium.org06928532015-02-03 02:11:29 +00005101 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005102 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005103 cmd = ['git', 'diff']
5104 if options.stat:
5105 cmd.append('--stat')
5106 cmd.extend([TMP_BRANCH, branch, '--'])
5107 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005108 finally:
5109 RunGit(['checkout', '-q', branch])
5110 RunGit(['branch', '-D', TMP_BRANCH])
5111
5112 return 0
5113
5114
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005115def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005116 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005117 parser.add_option(
5118 '--no-color',
5119 action='store_true',
5120 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005121 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005122 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005123 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005124
5125 author = RunGit(['config', 'user.email']).strip() or None
5126
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005127 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005128
5129 if args:
5130 if len(args) > 1:
5131 parser.error('Unknown args')
5132 base_branch = args[0]
5133 else:
5134 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005135 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005136
5137 change = cl.GetChange(base_branch, None)
5138 return owners_finder.OwnersFinder(
5139 [f.LocalPath() for f in
5140 cl.GetChange(base_branch, None).AffectedFiles()],
5141 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005142 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005143 disable_color=options.no_color).run()
5144
5145
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005146def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005147 """Generates a diff command."""
5148 # Generate diff for the current branch's changes.
5149 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
5150 upstream_commit, '--' ]
5151
5152 if args:
5153 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005154 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005155 diff_cmd.append(arg)
5156 else:
5157 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005158
5159 return diff_cmd
5160
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005161def MatchingFileType(file_name, extensions):
5162 """Returns true if the file name ends with one of the given extensions."""
5163 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005164
enne@chromium.org555cfe42014-01-29 18:21:39 +00005165@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005166def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005167 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07005168 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005169 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005170 parser.add_option('--full', action='store_true',
5171 help='Reformat the full content of all touched files')
5172 parser.add_option('--dry-run', action='store_true',
5173 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005174 parser.add_option('--python', action='store_true',
5175 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005176 parser.add_option('--diff', action='store_true',
5177 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005178 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005179
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005180 # git diff generates paths against the root of the repository. Change
5181 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005182 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005183 if rel_base_path:
5184 os.chdir(rel_base_path)
5185
digit@chromium.org29e47272013-05-17 17:01:46 +00005186 # Grab the merge-base commit, i.e. the upstream commit of the current
5187 # branch when it was created or the last time it was rebased. This is
5188 # to cover the case where the user may have called "git fetch origin",
5189 # moving the origin branch to a newer commit, but hasn't rebased yet.
5190 upstream_commit = None
5191 cl = Changelist()
5192 upstream_branch = cl.GetUpstreamBranch()
5193 if upstream_branch:
5194 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5195 upstream_commit = upstream_commit.strip()
5196
5197 if not upstream_commit:
5198 DieWithError('Could not find base commit for this branch. '
5199 'Are you in detached state?')
5200
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005201 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5202 diff_output = RunGit(changed_files_cmd)
5203 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005204 # Filter out files deleted by this CL
5205 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005206
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005207 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5208 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5209 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005210 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005211
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005212 top_dir = os.path.normpath(
5213 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5214
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005215 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5216 # formatted. This is used to block during the presubmit.
5217 return_value = 0
5218
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005219 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005220 # Locate the clang-format binary in the checkout
5221 try:
5222 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005223 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005224 DieWithError(e)
5225
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005226 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005227 cmd = [clang_format_tool]
5228 if not opts.dry_run and not opts.diff:
5229 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005230 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005231 if opts.diff:
5232 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005233 else:
5234 env = os.environ.copy()
5235 env['PATH'] = str(os.path.dirname(clang_format_tool))
5236 try:
5237 script = clang_format.FindClangFormatScriptInChromiumTree(
5238 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005239 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005240 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005241
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005242 cmd = [sys.executable, script, '-p0']
5243 if not opts.dry_run and not opts.diff:
5244 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005245
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005246 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5247 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005248
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005249 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5250 if opts.diff:
5251 sys.stdout.write(stdout)
5252 if opts.dry_run and len(stdout) > 0:
5253 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005254
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005255 # Similar code to above, but using yapf on .py files rather than clang-format
5256 # on C/C++ files
5257 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005258 yapf_tool = gclient_utils.FindExecutable('yapf')
5259 if yapf_tool is None:
5260 DieWithError('yapf not found in PATH')
5261
5262 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005263 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005264 cmd = [yapf_tool]
5265 if not opts.dry_run and not opts.diff:
5266 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005267 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005268 if opts.diff:
5269 sys.stdout.write(stdout)
5270 else:
5271 # TODO(sbc): yapf --lines mode still has some issues.
5272 # https://github.com/google/yapf/issues/154
5273 DieWithError('--python currently only works with --full')
5274
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005275 # Dart's formatter does not have the nice property of only operating on
5276 # modified chunks, so hard code full.
5277 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005278 try:
5279 command = [dart_format.FindDartFmtToolInChromiumTree()]
5280 if not opts.dry_run and not opts.diff:
5281 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005282 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005283
ppi@chromium.org6593d932016-03-03 15:41:15 +00005284 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005285 if opts.dry_run and stdout:
5286 return_value = 2
5287 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005288 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5289 'found in this checkout. Files in other languages are still '
5290 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005291
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005292 # Format GN build files. Always run on full build files for canonical form.
5293 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005294 cmd = ['gn', 'format' ]
5295 if opts.dry_run or opts.diff:
5296 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005297 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005298 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5299 shell=sys.platform == 'win32',
5300 cwd=top_dir)
5301 if opts.dry_run and gn_ret == 2:
5302 return_value = 2 # Not formatted.
5303 elif opts.diff and gn_ret == 2:
5304 # TODO this should compute and print the actual diff.
5305 print("This change has GN build file diff for " + gn_diff_file)
5306 elif gn_ret != 0:
5307 # For non-dry run cases (and non-2 return values for dry-run), a
5308 # nonzero error code indicates a failure, probably because the file
5309 # doesn't parse.
5310 DieWithError("gn format failed on " + gn_diff_file +
5311 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005312
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005313 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005314
5315
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005316@subcommand.usage('<codereview url or issue id>')
5317def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005318 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005319 _, args = parser.parse_args(args)
5320
5321 if len(args) != 1:
5322 parser.print_help()
5323 return 1
5324
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005325 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005326 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005327 parser.print_help()
5328 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005329 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005330
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005331 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005332 output = RunGit(['config', '--local', '--get-regexp',
5333 r'branch\..*\.%s' % issueprefix],
5334 error_ok=True)
5335 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005336 if issue == target_issue:
5337 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005338
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005339 branches = []
5340 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005341 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005342 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005343 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005344 return 1
5345 if len(branches) == 1:
5346 RunGit(['checkout', branches[0]])
5347 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005348 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005349 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005350 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005351 which = raw_input('Choose by index: ')
5352 try:
5353 RunGit(['checkout', branches[int(which)]])
5354 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005355 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005356 return 1
5357
5358 return 0
5359
5360
maruel@chromium.org29404b52014-09-08 22:58:00 +00005361def CMDlol(parser, args):
5362 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005363 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005364 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5365 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5366 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005367 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005368 return 0
5369
5370
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005371class OptionParser(optparse.OptionParser):
5372 """Creates the option parse and add --verbose support."""
5373 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005374 optparse.OptionParser.__init__(
5375 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005376 self.add_option(
5377 '-v', '--verbose', action='count', default=0,
5378 help='Use 2 times for more debugging info')
5379
5380 def parse_args(self, args=None, values=None):
5381 options, args = optparse.OptionParser.parse_args(self, args, values)
5382 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5383 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5384 return options, args
5385
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005386
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005387def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005388 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005389 print('\nYour python version %s is unsupported, please upgrade.\n' %
5390 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005391 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005392
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005393 # Reload settings.
5394 global settings
5395 settings = Settings()
5396
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005397 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005398 dispatcher = subcommand.CommandDispatcher(__name__)
5399 try:
5400 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005401 except auth.AuthenticationError as e:
5402 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005403 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005404 if e.code != 500:
5405 raise
5406 DieWithError(
5407 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5408 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005409 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005410
5411
5412if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005413 # These affect sys.stdout so do it outside of main() to simplify mocks in
5414 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005415 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005416 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005417 try:
5418 sys.exit(main(sys.argv[1:]))
5419 except KeyboardInterrupt:
5420 sys.stderr.write('interrupted\n')
5421 sys.exit(1)