blob: 4ca54e2ffabc9b9ef2b2c7d8718ae7e4443bc079 [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000012from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000013from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000014import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000015import collections
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +010016import fnmatch
sheyang@google.com6ebaf782015-05-12 19:17:54 +000017import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000018import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000020import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import optparse
22import os
23import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000024import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000026import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000027import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000028import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000030import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000031import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000032import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000033import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034
35try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080036 import readline # pylint: disable=import-error,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000037except ImportError:
38 pass
39
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000040from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000041from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000043import auth
skobes6468b902016-10-24 08:45:10 -070044import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000045import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000046import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000047import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000048import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000049import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000050import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000051import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000052import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000053import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000054import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000055import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000056import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000057import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000058import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000059import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000060import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000061import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062import watchlists
63
tandrii7400cf02016-06-21 08:48:07 -070064__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000065
tandrii9d2c7a32016-06-22 03:42:45 -070066COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070067DEFAULT_SERVER = 'https://codereview.chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080068POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000069DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +000070REFS_THAT_ALIAS_TO_OTHER_REFS = {
71 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
72 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
73}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074
thestig@chromium.org44202a22014-03-11 19:22:18 +000075# Valid extensions for files we want to lint.
76DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
77DEFAULT_LINT_IGNORE_REGEX = r"$^"
78
borenet6c0efe62016-10-19 08:13:29 -070079# Buildbucket master name prefix.
80MASTER_PREFIX = 'master.'
81
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000082# Shortcut since it quickly becomes redundant.
83Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000084
maruel@chromium.orgddd59412011-11-30 14:20:38 +000085# Initialized in main()
86settings = None
87
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010088# Used by tests/git_cl_test.py to add extra logging.
89# Inside the weirdly failing test, add this:
90# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
91# And scroll up to see the strack trace printed.
92_IS_BEING_TESTED = False
93
maruel@chromium.orgddd59412011-11-30 14:20:38 +000094
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000095def DieWithError(message):
vapiera7fbd5a2016-06-16 09:17:49 -070096 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000097 sys.exit(1)
98
99
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000100def GetNoGitPagerEnv():
101 env = os.environ.copy()
102 # 'cat' is a magical git string that disables pagers on all platforms.
103 env['GIT_PAGER'] = 'cat'
104 return env
105
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000106
bsep@chromium.org627d9002016-04-29 00:00:52 +0000107def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000108 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000109 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000110 except subprocess2.CalledProcessError as e:
111 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000112 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000113 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000114 'Command "%s" failed.\n%s' % (
115 ' '.join(args), error_message or e.stdout or ''))
116 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000117
118
119def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000120 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000121 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000122
123
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000124def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000125 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700126 if suppress_stderr:
127 stderr = subprocess2.VOID
128 else:
129 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000130 try:
tandrii5d48c322016-08-18 16:19:37 -0700131 (out, _), code = subprocess2.communicate(['git'] + args,
132 env=GetNoGitPagerEnv(),
133 stdout=subprocess2.PIPE,
134 stderr=stderr)
135 return code, out
136 except subprocess2.CalledProcessError as e:
137 logging.debug('Failed running %s', args)
138 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000139
140
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000141def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000142 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000143 return RunGitWithCode(args, suppress_stderr=True)[1]
144
145
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000146def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000147 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000148 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000149 return (version.startswith(prefix) and
150 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000151
152
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000153def BranchExists(branch):
154 """Return True if specified branch exists."""
155 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
156 suppress_stderr=True)
157 return not code
158
159
tandrii2a16b952016-10-19 07:09:44 -0700160def time_sleep(seconds):
161 # Use this so that it can be mocked in tests without interfering with python
162 # system machinery.
163 import time # Local import to discourage others from importing time globally.
164 return time.sleep(seconds)
165
166
maruel@chromium.org90541732011-04-01 17:54:18 +0000167def ask_for_data(prompt):
168 try:
169 return raw_input(prompt)
170 except KeyboardInterrupt:
171 # Hide the exception.
172 sys.exit(1)
173
174
tandrii5d48c322016-08-18 16:19:37 -0700175def _git_branch_config_key(branch, key):
176 """Helper method to return Git config key for a branch."""
177 assert branch, 'branch name is required to set git config for it'
178 return 'branch.%s.%s' % (branch, key)
179
180
181def _git_get_branch_config_value(key, default=None, value_type=str,
182 branch=False):
183 """Returns git config value of given or current branch if any.
184
185 Returns default in all other cases.
186 """
187 assert value_type in (int, str, bool)
188 if branch is False: # Distinguishing default arg value from None.
189 branch = GetCurrentBranch()
190
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000191 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700192 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000193
tandrii5d48c322016-08-18 16:19:37 -0700194 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700195 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700196 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700197 # git config also has --int, but apparently git config suffers from integer
198 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700199 args.append(_git_branch_config_key(branch, key))
200 code, out = RunGitWithCode(args)
201 if code == 0:
202 value = out.strip()
203 if value_type == int:
204 return int(value)
205 if value_type == bool:
206 return bool(value.lower() == 'true')
207 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000208 return default
209
210
tandrii5d48c322016-08-18 16:19:37 -0700211def _git_set_branch_config_value(key, value, branch=None, **kwargs):
212 """Sets the value or unsets if it's None of a git branch config.
213
214 Valid, though not necessarily existing, branch must be provided,
215 otherwise currently checked out branch is used.
216 """
217 if not branch:
218 branch = GetCurrentBranch()
219 assert branch, 'a branch name OR currently checked out branch is required'
220 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700221 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700222 if value is None:
223 args.append('--unset')
224 elif isinstance(value, bool):
225 args.append('--bool')
226 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700227 else:
tandrii33a46ff2016-08-23 05:53:40 -0700228 # git config also has --int, but apparently git config suffers from integer
229 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700230 value = str(value)
231 args.append(_git_branch_config_key(branch, key))
232 if value is not None:
233 args.append(value)
234 RunGit(args, **kwargs)
235
236
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100237def _get_committer_timestamp(commit):
238 """Returns unix timestamp as integer of a committer in a commit.
239
240 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
241 """
242 # Git also stores timezone offset, but it only affects visual display,
243 # actual point in time is defined by this timestamp only.
244 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
245
246
247def _git_amend_head(message, committer_timestamp):
248 """Amends commit with new message and desired committer_timestamp.
249
250 Sets committer timezone to UTC.
251 """
252 env = os.environ.copy()
253 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
254 return RunGit(['commit', '--amend', '-m', message], env=env)
255
256
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000257def add_git_similarity(parser):
258 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700259 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000260 help='Sets the percentage that a pair of files need to match in order to'
261 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000262 parser.add_option(
263 '--find-copies', action='store_true',
264 help='Allows git to look for copies.')
265 parser.add_option(
266 '--no-find-copies', action='store_false', dest='find_copies',
267 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000268
269 old_parser_args = parser.parse_args
270 def Parse(args):
271 options, args = old_parser_args(args)
272
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000273 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700274 options.similarity = _git_get_branch_config_value(
275 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000276 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000277 print('Note: Saving similarity of %d%% in git config.'
278 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700279 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000280
iannucci@chromium.org79540052012-10-19 23:15:26 +0000281 options.similarity = max(0, min(options.similarity, 100))
282
283 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700284 options.find_copies = _git_get_branch_config_value(
285 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000286 else:
tandrii5d48c322016-08-18 16:19:37 -0700287 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000288
289 print('Using %d%% similarity for rename/copy detection. '
290 'Override with --similarity.' % options.similarity)
291
292 return options, args
293 parser.parse_args = Parse
294
295
machenbach@chromium.org45453142015-09-15 08:45:22 +0000296def _get_properties_from_options(options):
297 properties = dict(x.split('=', 1) for x in options.properties)
298 for key, val in properties.iteritems():
299 try:
300 properties[key] = json.loads(val)
301 except ValueError:
302 pass # If a value couldn't be evaluated, treat it as a string.
303 return properties
304
305
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000306def _prefix_master(master):
307 """Convert user-specified master name to full master name.
308
309 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
310 name, while the developers always use shortened master name
311 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
312 function does the conversion for buildbucket migration.
313 """
borenet6c0efe62016-10-19 08:13:29 -0700314 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000315 return master
borenet6c0efe62016-10-19 08:13:29 -0700316 return '%s%s' % (MASTER_PREFIX, master)
317
318
319def _unprefix_master(bucket):
320 """Convert bucket name to shortened master name.
321
322 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
323 name, while the developers always use shortened master name
324 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
325 function does the conversion for buildbucket migration.
326 """
327 if bucket.startswith(MASTER_PREFIX):
328 return bucket[len(MASTER_PREFIX):]
329 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000330
331
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000332def _buildbucket_retry(operation_name, http, *args, **kwargs):
333 """Retries requests to buildbucket service and returns parsed json content."""
334 try_count = 0
335 while True:
336 response, content = http.request(*args, **kwargs)
337 try:
338 content_json = json.loads(content)
339 except ValueError:
340 content_json = None
341
342 # Buildbucket could return an error even if status==200.
343 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000344 error = content_json.get('error')
345 if error.get('code') == 403:
346 raise BuildbucketResponseException(
347 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000348 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000349 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000350 raise BuildbucketResponseException(msg)
351
352 if response.status == 200:
353 if not content_json:
354 raise BuildbucketResponseException(
355 'Buildbucket returns invalid json content: %s.\n'
356 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
357 content)
358 return content_json
359 if response.status < 500 or try_count >= 2:
360 raise httplib2.HttpLib2Error(content)
361
362 # status >= 500 means transient failures.
363 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700364 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000365 try_count += 1
366 assert False, 'unreachable'
367
368
qyearsley1fdfcb62016-10-24 13:22:03 -0700369def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700370 """Returns a dict mapping bucket names to builders and tests,
371 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700372 """
qyearsleydd49f942016-10-28 11:57:22 -0700373 # If no bots are listed, we try to get a set of builders and tests based
374 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700375 if not options.bot:
376 change = changelist.GetChange(
377 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700378 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700379 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700380 change=change,
381 changed_files=change.LocalPaths(),
382 repository_root=settings.GetRoot(),
383 default_presubmit=None,
384 project=None,
385 verbose=options.verbose,
386 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700387 if masters is None:
388 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100389 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700390
qyearsley1fdfcb62016-10-24 13:22:03 -0700391 if options.bucket:
392 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700393 if options.master:
394 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700395
qyearsleydd49f942016-10-28 11:57:22 -0700396 # If bots are listed but no master or bucket, then we need to find out
397 # the corresponding master for each bot.
398 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
399 if error_message:
400 option_parser.error(
401 'Tryserver master cannot be found because: %s\n'
402 'Please manually specify the tryserver master, e.g. '
403 '"-m tryserver.chromium.linux".' % error_message)
404 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700405
406
qyearsley123a4682016-10-26 09:12:17 -0700407def _get_bucket_map_for_builders(builders):
408 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700409 map_url = 'https://builders-map.appspot.com/'
410 try:
qyearsley123a4682016-10-26 09:12:17 -0700411 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700412 except urllib2.URLError as e:
413 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
414 (map_url, e))
415 except ValueError as e:
416 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700417 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700418 return None, 'Failed to build master map.'
419
qyearsley123a4682016-10-26 09:12:17 -0700420 bucket_map = {}
421 for builder in builders:
qyearsley123a4682016-10-26 09:12:17 -0700422 masters = builders_map.get(builder, [])
423 if not masters:
qyearsley1fdfcb62016-10-24 13:22:03 -0700424 return None, ('No matching master for builder %s.' % builder)
qyearsley123a4682016-10-26 09:12:17 -0700425 if len(masters) > 1:
qyearsley1fdfcb62016-10-24 13:22:03 -0700426 return None, ('The builder name %s exists in multiple masters %s.' %
qyearsley123a4682016-10-26 09:12:17 -0700427 (builder, masters))
428 bucket = _prefix_master(masters[0])
429 bucket_map.setdefault(bucket, {})[builder] = []
430
431 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700432
433
borenet6c0efe62016-10-19 08:13:29 -0700434def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700435 category='git_cl_try', patchset=None):
qyearsley1fdfcb62016-10-24 13:22:03 -0700436 """Sends a request to Buildbucket to trigger try jobs for a changelist.
437
438 Args:
439 auth_config: AuthConfig for Rietveld.
440 changelist: Changelist that the try jobs are associated with.
441 buckets: A nested dict mapping bucket names to builders to tests.
442 options: Command-line options.
443 """
tandriide281ae2016-10-12 06:02:30 -0700444 assert changelist.GetIssue(), 'CL must be uploaded first'
445 codereview_url = changelist.GetCodereviewServer()
446 assert codereview_url, 'CL must be uploaded first'
447 patchset = patchset or changelist.GetMostRecentPatchset()
448 assert patchset, 'CL must be uploaded first'
449
450 codereview_host = urlparse.urlparse(codereview_url).hostname
451 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000452 http = authenticator.authorize(httplib2.Http())
453 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700454
455 # TODO(tandrii): consider caching Gerrit CL details just like
456 # _RietveldChangelistImpl does, then caching values in these two variables
457 # won't be necessary.
458 owner_email = changelist.GetIssueOwner()
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000459
460 buildbucket_put_url = (
461 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000462 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700463 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
464 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
465 hostname=codereview_host,
466 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000467 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700468
469 shared_parameters_properties = changelist.GetTryjobProperties(patchset)
470 shared_parameters_properties['category'] = category
471 if options.clobber:
472 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700473 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700474 if extra_properties:
475 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000476
477 batch_req_body = {'builds': []}
478 print_text = []
479 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700480 for bucket, builders_and_tests in sorted(buckets.iteritems()):
481 print_text.append('Bucket: %s' % bucket)
482 master = None
483 if bucket.startswith(MASTER_PREFIX):
484 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000485 for builder, tests in sorted(builders_and_tests.iteritems()):
486 print_text.append(' %s: %s' % (builder, tests))
487 parameters = {
488 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000489 'changes': [{
tandriide281ae2016-10-12 06:02:30 -0700490 'author': {'email': owner_email},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000491 'revision': options.revision,
492 }],
tandrii8c5a3532016-11-04 07:52:02 -0700493 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000494 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000495 if 'presubmit' in builder.lower():
496 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000497 if tests:
498 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700499
500 tags = [
501 'builder:%s' % builder,
502 'buildset:%s' % buildset,
503 'user_agent:git_cl_try',
504 ]
505 if master:
506 parameters['properties']['master'] = master
507 tags.append('master:%s' % master)
508
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000509 batch_req_body['builds'].append(
510 {
511 'bucket': bucket,
512 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000513 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700514 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000515 }
516 )
517
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000518 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700519 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000520 http,
521 buildbucket_put_url,
522 'PUT',
523 body=json.dumps(batch_req_body),
524 headers={'Content-Type': 'application/json'}
525 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000526 print_text.append('To see results here, run: git cl try-results')
527 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700528 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000529
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000530
tandrii221ab252016-10-06 08:12:04 -0700531def fetch_try_jobs(auth_config, changelist, buildbucket_host,
532 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700533 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000534
qyearsley53f48a12016-09-01 10:45:13 -0700535 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000536 """
tandrii221ab252016-10-06 08:12:04 -0700537 assert buildbucket_host
538 assert changelist.GetIssue(), 'CL must be uploaded first'
539 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
540 patchset = patchset or changelist.GetMostRecentPatchset()
541 assert patchset, 'CL must be uploaded first'
542
543 codereview_url = changelist.GetCodereviewServer()
544 codereview_host = urlparse.urlparse(codereview_url).hostname
545 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000546 if authenticator.has_cached_credentials():
547 http = authenticator.authorize(httplib2.Http())
548 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700549 print('Warning: Some results might be missing because %s' %
550 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700551 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000552 http = httplib2.Http()
553
554 http.force_exception_to_status_code = True
555
tandrii221ab252016-10-06 08:12:04 -0700556 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
557 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
558 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000559 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700560 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000561 params = {'tag': 'buildset:%s' % buildset}
562
563 builds = {}
564 while True:
565 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700566 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000567 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700568 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000569 for build in content.get('builds', []):
570 builds[build['id']] = build
571 if 'next_cursor' in content:
572 params['start_cursor'] = content['next_cursor']
573 else:
574 break
575 return builds
576
577
qyearsleyeab3c042016-08-24 09:18:28 -0700578def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000579 """Prints nicely result of fetch_try_jobs."""
580 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700581 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000582 return
583
584 # Make a copy, because we'll be modifying builds dictionary.
585 builds = builds.copy()
586 builder_names_cache = {}
587
588 def get_builder(b):
589 try:
590 return builder_names_cache[b['id']]
591 except KeyError:
592 try:
593 parameters = json.loads(b['parameters_json'])
594 name = parameters['builder_name']
595 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700596 print('WARNING: failed to get builder name for build %s: %s' % (
597 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000598 name = None
599 builder_names_cache[b['id']] = name
600 return name
601
602 def get_bucket(b):
603 bucket = b['bucket']
604 if bucket.startswith('master.'):
605 return bucket[len('master.'):]
606 return bucket
607
608 if options.print_master:
609 name_fmt = '%%-%ds %%-%ds' % (
610 max(len(str(get_bucket(b))) for b in builds.itervalues()),
611 max(len(str(get_builder(b))) for b in builds.itervalues()))
612 def get_name(b):
613 return name_fmt % (get_bucket(b), get_builder(b))
614 else:
615 name_fmt = '%%-%ds' % (
616 max(len(str(get_builder(b))) for b in builds.itervalues()))
617 def get_name(b):
618 return name_fmt % get_builder(b)
619
620 def sort_key(b):
621 return b['status'], b.get('result'), get_name(b), b.get('url')
622
623 def pop(title, f, color=None, **kwargs):
624 """Pop matching builds from `builds` dict and print them."""
625
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000626 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000627 colorize = str
628 else:
629 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
630
631 result = []
632 for b in builds.values():
633 if all(b.get(k) == v for k, v in kwargs.iteritems()):
634 builds.pop(b['id'])
635 result.append(b)
636 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700637 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000638 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700639 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000640
641 total = len(builds)
642 pop(status='COMPLETED', result='SUCCESS',
643 title='Successes:', color=Fore.GREEN,
644 f=lambda b: (get_name(b), b.get('url')))
645 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
646 title='Infra Failures:', color=Fore.MAGENTA,
647 f=lambda b: (get_name(b), b.get('url')))
648 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
649 title='Failures:', color=Fore.RED,
650 f=lambda b: (get_name(b), b.get('url')))
651 pop(status='COMPLETED', result='CANCELED',
652 title='Canceled:', color=Fore.MAGENTA,
653 f=lambda b: (get_name(b),))
654 pop(status='COMPLETED', result='FAILURE',
655 failure_reason='INVALID_BUILD_DEFINITION',
656 title='Wrong master/builder name:', color=Fore.MAGENTA,
657 f=lambda b: (get_name(b),))
658 pop(status='COMPLETED', result='FAILURE',
659 title='Other failures:',
660 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
661 pop(status='COMPLETED',
662 title='Other finished:',
663 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
664 pop(status='STARTED',
665 title='Started:', color=Fore.YELLOW,
666 f=lambda b: (get_name(b), b.get('url')))
667 pop(status='SCHEDULED',
668 title='Scheduled:',
669 f=lambda b: (get_name(b), 'id=%s' % b['id']))
670 # The last section is just in case buildbucket API changes OR there is a bug.
671 pop(title='Other:',
672 f=lambda b: (get_name(b), 'id=%s' % b['id']))
673 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700674 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000675
676
qyearsley53f48a12016-09-01 10:45:13 -0700677def write_try_results_json(output_file, builds):
678 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
679
680 The input |builds| dict is assumed to be generated by Buildbucket.
681 Buildbucket documentation: http://goo.gl/G0s101
682 """
683
684 def convert_build_dict(build):
685 return {
686 'buildbucket_id': build.get('id'),
687 'status': build.get('status'),
688 'result': build.get('result'),
689 'bucket': build.get('bucket'),
690 'builder_name': json.loads(
691 build.get('parameters_json', '{}')).get('builder_name'),
692 'failure_reason': build.get('failure_reason'),
693 'url': build.get('url'),
694 }
695
696 converted = []
697 for _, build in sorted(builds.items()):
698 converted.append(convert_build_dict(build))
699 write_json(output_file, converted)
700
701
iannucci@chromium.org79540052012-10-19 23:15:26 +0000702def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000703 """Prints statistics about the change to the user."""
704 # --no-ext-diff is broken in some versions of Git, so try to work around
705 # this by overriding the environment (but there is still a problem if the
706 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000707 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000708 if 'GIT_EXTERNAL_DIFF' in env:
709 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000710
711 if find_copies:
scottmgb84b5e32016-11-10 09:25:33 -0800712 similarity_options = ['-l100000', '-C%s' % similarity]
iannucci@chromium.org79540052012-10-19 23:15:26 +0000713 else:
714 similarity_options = ['-M%s' % similarity]
715
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000716 try:
717 stdout = sys.stdout.fileno()
718 except AttributeError:
719 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000720 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000721 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000722 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000723 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000724
725
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000726class BuildbucketResponseException(Exception):
727 pass
728
729
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000730class Settings(object):
731 def __init__(self):
732 self.default_server = None
733 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000734 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000735 self.tree_status_url = None
736 self.viewvc_url = None
737 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000738 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000739 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000740 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000741 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000742 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000743 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000744 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000745
746 def LazyUpdateIfNeeded(self):
747 """Updates the settings from a codereview.settings file, if available."""
748 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000749 # The only value that actually changes the behavior is
750 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000751 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000752 error_ok=True
753 ).strip().lower()
754
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000755 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000756 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000757 LoadCodereviewSettingsFromFile(cr_settings_file)
758 self.updated = True
759
760 def GetDefaultServerUrl(self, error_ok=False):
761 if not self.default_server:
762 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000763 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000764 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000765 if error_ok:
766 return self.default_server
767 if not self.default_server:
768 error_message = ('Could not find settings file. You must configure '
769 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000770 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000771 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000772 return self.default_server
773
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000774 @staticmethod
775 def GetRelativeRoot():
776 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000777
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000778 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000779 if self.root is None:
780 self.root = os.path.abspath(self.GetRelativeRoot())
781 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000782
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000783 def GetGitMirror(self, remote='origin'):
784 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000785 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000786 if not os.path.isdir(local_url):
787 return None
788 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
789 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
790 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
791 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
792 if mirror.exists():
793 return mirror
794 return None
795
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000796 def GetTreeStatusUrl(self, error_ok=False):
797 if not self.tree_status_url:
798 error_message = ('You must configure your tree status URL by running '
799 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000800 self.tree_status_url = self._GetRietveldConfig(
801 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000802 return self.tree_status_url
803
804 def GetViewVCUrl(self):
805 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000806 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000807 return self.viewvc_url
808
rmistry@google.com90752582014-01-14 21:04:50 +0000809 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000810 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000811
rmistry@google.com78948ed2015-07-08 23:09:57 +0000812 def GetIsSkipDependencyUpload(self, branch_name):
813 """Returns true if specified branch should skip dep uploads."""
814 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
815 error_ok=True)
816
rmistry@google.com5626a922015-02-26 14:03:30 +0000817 def GetRunPostUploadHook(self):
818 run_post_upload_hook = self._GetRietveldConfig(
819 'run-post-upload-hook', error_ok=True)
820 return run_post_upload_hook == "True"
821
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000822 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000823 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000824
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000825 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000826 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000827
ukai@chromium.orge8077812012-02-03 03:41:46 +0000828 def GetIsGerrit(self):
829 """Return true if this repo is assosiated with gerrit code review system."""
830 if self.is_gerrit is None:
831 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
832 return self.is_gerrit
833
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000834 def GetSquashGerritUploads(self):
835 """Return true if uploads to Gerrit should be squashed by default."""
836 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700837 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
838 if self.squash_gerrit_uploads is None:
839 # Default is squash now (http://crbug.com/611892#c23).
840 self.squash_gerrit_uploads = not (
841 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
842 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000843 return self.squash_gerrit_uploads
844
tandriia60502f2016-06-20 02:01:53 -0700845 def GetSquashGerritUploadsOverride(self):
846 """Return True or False if codereview.settings should be overridden.
847
848 Returns None if no override has been defined.
849 """
850 # See also http://crbug.com/611892#c23
851 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
852 error_ok=True).strip()
853 if result == 'true':
854 return True
855 if result == 'false':
856 return False
857 return None
858
tandrii@chromium.org28253532016-04-14 13:46:56 +0000859 def GetGerritSkipEnsureAuthenticated(self):
860 """Return True if EnsureAuthenticated should not be done for Gerrit
861 uploads."""
862 if self.gerrit_skip_ensure_authenticated is None:
863 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000864 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000865 error_ok=True).strip() == 'true')
866 return self.gerrit_skip_ensure_authenticated
867
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000868 def GetGitEditor(self):
869 """Return the editor specified in the git config, or None if none is."""
870 if self.git_editor is None:
871 self.git_editor = self._GetConfig('core.editor', error_ok=True)
872 return self.git_editor or None
873
thestig@chromium.org44202a22014-03-11 19:22:18 +0000874 def GetLintRegex(self):
875 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
876 DEFAULT_LINT_REGEX)
877
878 def GetLintIgnoreRegex(self):
879 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
880 DEFAULT_LINT_IGNORE_REGEX)
881
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000882 def GetProject(self):
883 if not self.project:
884 self.project = self._GetRietveldConfig('project', error_ok=True)
885 return self.project
886
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000887 def GetPendingRefPrefix(self):
888 if not self.pending_ref_prefix:
889 self.pending_ref_prefix = self._GetRietveldConfig(
890 'pending-ref-prefix', error_ok=True)
891 return self.pending_ref_prefix
892
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000893 def _GetRietveldConfig(self, param, **kwargs):
894 return self._GetConfig('rietveld.' + param, **kwargs)
895
rmistry@google.com78948ed2015-07-08 23:09:57 +0000896 def _GetBranchConfig(self, branch_name, param, **kwargs):
897 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
898
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000899 def _GetConfig(self, param, **kwargs):
900 self.LazyUpdateIfNeeded()
901 return RunGit(['config', param], **kwargs).strip()
902
903
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100904class _GitNumbererState(object):
905 KNOWN_PROJECTS_WHITELIST = [
906 'chromium/src',
907 'external/webrtc',
908 'v8/v8',
909 ]
910
911 @classmethod
912 def load(cls, remote_url, remote_ref):
913 """Figures out the state by fetching special refs from remote repo.
914 """
915 assert remote_ref and remote_ref.startswith('refs/'), remote_ref
916 url_parts = urlparse.urlparse(remote_url)
917 project_name = url_parts.path.lstrip('/').rstrip('git./')
918 for known in cls.KNOWN_PROJECTS_WHITELIST:
919 if project_name.endswith(known):
920 break
921 else:
922 # Early exit to avoid extra fetches for repos that aren't using gnumbd.
923 return cls(cls._get_pending_prefix_fallback(), None)
924
Quinten Yearsley442fb642016-12-15 15:38:27 -0800925 # This pollutes local ref space, but the amount of objects is negligible.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100926 error, _ = cls._run_git_with_code([
927 'fetch', remote_url,
928 '+refs/meta/config:refs/git_cl/meta/config',
929 '+refs/gnumbd-config/main:refs/git_cl/gnumbd-config/main'])
930 if error:
931 # Some ref doesn't exist or isn't accessible to current user.
932 # This shouldn't happen on production KNOWN_PROJECTS_WHITELIST
933 # with git-numberer.
934 cls._warn('failed to fetch gnumbd and project config for %s: %s',
935 remote_url, error)
936 return cls(cls._get_pending_prefix_fallback(), None)
937 return cls(cls._get_pending_prefix(remote_ref),
938 cls._is_validator_enabled(remote_ref))
939
940 @classmethod
941 def _get_pending_prefix(cls, ref):
942 error, gnumbd_config_data = cls._run_git_with_code(
943 ['show', 'refs/git_cl/gnumbd-config/main:config.json'])
944 if error:
945 cls._warn('gnumbd config file not found')
946 return cls._get_pending_prefix_fallback()
947
948 try:
949 config = json.loads(gnumbd_config_data)
950 if cls.match_refglobs(ref, config['enabled_refglobs']):
951 return config['pending_ref_prefix']
952 return None
953 except KeyboardInterrupt:
954 raise
955 except Exception as e:
956 cls._warn('failed to parse gnumbd config: %s', e)
957 return cls._get_pending_prefix_fallback()
958
959 @staticmethod
960 def _get_pending_prefix_fallback():
961 global settings
962 if not settings:
963 settings = Settings()
964 return settings.GetPendingRefPrefix()
965
966 @classmethod
967 def _is_validator_enabled(cls, ref):
968 error, project_config_data = cls._run_git_with_code(
969 ['show', 'refs/git_cl/meta/config:project.config'])
970 if error:
971 cls._warn('project.config file not found')
972 return False
973 # Gerrit's project.config is really a git config file.
974 # So, parse it as such.
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000975 with gclient_utils.temporary_directory() as tempdir:
976 project_config_file = os.path.join(tempdir, 'project.config')
977 gclient_utils.FileWrite(project_config_file, project_config_data)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100978
979 def get_opts(x):
980 code, out = cls._run_git_with_code(
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000981 ['config', '-f', project_config_file, '--get-all',
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100982 'plugin.git-numberer.validate-%s-refglob' % x])
983 if code == 0:
984 return out.strip().splitlines()
985 return []
986 enabled, disabled = map(get_opts, ['enabled', 'disabled'])
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000987
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100988 logging.info('validator config enabled %s disabled %s refglobs for '
989 '(this ref: %s)', enabled, disabled, ref)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100990
991 if cls.match_refglobs(ref, disabled):
992 return False
993 return cls.match_refglobs(ref, enabled)
994
995 @staticmethod
996 def match_refglobs(ref, refglobs):
997 for refglob in refglobs:
998 if ref == refglob or fnmatch.fnmatch(ref, refglob):
999 return True
1000 return False
1001
1002 @staticmethod
1003 def _run_git_with_code(*args, **kwargs):
1004 # The only reason for this wrapper is easy porting of this code to CQ
1005 # codebase, which forked git_cl.py and checkouts.py long time ago.
1006 return RunGitWithCode(*args, **kwargs)
1007
1008 @staticmethod
1009 def _warn(msg, *args):
1010 if args:
1011 msg = msg % args
1012 print('WARNING: %s' % msg)
1013
1014 def __init__(self, pending_prefix, validator_enabled):
1015 # TODO(tandrii): remove pending_prefix after gnumbd is no more.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01001016 if pending_prefix:
1017 if not pending_prefix.endswith('/'):
1018 pending_prefix += '/'
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001019 self._pending_prefix = pending_prefix or None
1020 self._validator_enabled = validator_enabled or False
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01001021 logging.debug('_GitNumbererState(pending: %s, validator: %s)',
1022 self._pending_prefix, self._validator_enabled)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001023
1024 @property
1025 def pending_prefix(self):
1026 return self._pending_prefix
1027
1028 @property
Andrii Shyshkalov8f15f3e2016-12-14 15:43:49 +01001029 def should_add_git_number(self):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001030 return self._validator_enabled and self._pending_prefix is None
1031
1032
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001033def ShortBranchName(branch):
1034 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001035 return branch.replace('refs/heads/', '', 1)
1036
1037
1038def GetCurrentBranchRef():
1039 """Returns branch ref (e.g., refs/heads/master) or None."""
1040 return RunGit(['symbolic-ref', 'HEAD'],
1041 stderr=subprocess2.VOID, error_ok=True).strip() or None
1042
1043
1044def GetCurrentBranch():
1045 """Returns current branch or None.
1046
1047 For refs/heads/* branches, returns just last part. For others, full ref.
1048 """
1049 branchref = GetCurrentBranchRef()
1050 if branchref:
1051 return ShortBranchName(branchref)
1052 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001053
1054
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001055class _CQState(object):
1056 """Enum for states of CL with respect to Commit Queue."""
1057 NONE = 'none'
1058 DRY_RUN = 'dry_run'
1059 COMMIT = 'commit'
1060
1061 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1062
1063
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001064class _ParsedIssueNumberArgument(object):
1065 def __init__(self, issue=None, patchset=None, hostname=None):
1066 self.issue = issue
1067 self.patchset = patchset
1068 self.hostname = hostname
1069
1070 @property
1071 def valid(self):
1072 return self.issue is not None
1073
1074
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001075def ParseIssueNumberArgument(arg):
1076 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1077 fail_result = _ParsedIssueNumberArgument()
1078
1079 if arg.isdigit():
1080 return _ParsedIssueNumberArgument(issue=int(arg))
1081 if not arg.startswith('http'):
1082 return fail_result
1083 url = gclient_utils.UpgradeToHttps(arg)
1084 try:
1085 parsed_url = urlparse.urlparse(url)
1086 except ValueError:
1087 return fail_result
1088 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
1089 tmp = cls.ParseIssueURL(parsed_url)
1090 if tmp is not None:
1091 return tmp
1092 return fail_result
1093
1094
Aaron Gablea45ee112016-11-22 15:14:38 -08001095class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001096 def __init__(self, issue, url):
1097 self.issue = issue
1098 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001099 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001100
1101 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001102 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001103 self.issue, self.url)
1104
1105
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001106class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001107 """Changelist works with one changelist in local branch.
1108
1109 Supports two codereview backends: Rietveld or Gerrit, selected at object
1110 creation.
1111
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001112 Notes:
1113 * Not safe for concurrent multi-{thread,process} use.
1114 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001115 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001116 """
1117
1118 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1119 """Create a new ChangeList instance.
1120
1121 If issue is given, the codereview must be given too.
1122
1123 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1124 Otherwise, it's decided based on current configuration of the local branch,
1125 with default being 'rietveld' for backwards compatibility.
1126 See _load_codereview_impl for more details.
1127
1128 **kwargs will be passed directly to codereview implementation.
1129 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001130 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001131 global settings
1132 if not settings:
1133 # Happens when git_cl.py is used as a utility library.
1134 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001135
1136 if issue:
1137 assert codereview, 'codereview must be known, if issue is known'
1138
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001139 self.branchref = branchref
1140 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001141 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001142 self.branch = ShortBranchName(self.branchref)
1143 else:
1144 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001145 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001146 self.lookedup_issue = False
1147 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001148 self.has_description = False
1149 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001150 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001151 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001152 self.cc = None
1153 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001154 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001155
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001156 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001157 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001158 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001159 assert self._codereview_impl
1160 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001161
1162 def _load_codereview_impl(self, codereview=None, **kwargs):
1163 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001164 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1165 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1166 self._codereview = codereview
1167 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001168 return
1169
1170 # Automatic selection based on issue number set for a current branch.
1171 # Rietveld takes precedence over Gerrit.
1172 assert not self.issue
1173 # Whether we find issue or not, we are doing the lookup.
1174 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001175 if self.GetBranch():
1176 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1177 issue = _git_get_branch_config_value(
1178 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1179 if issue:
1180 self._codereview = codereview
1181 self._codereview_impl = cls(self, **kwargs)
1182 self.issue = int(issue)
1183 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001184
1185 # No issue is set for this branch, so decide based on repo-wide settings.
1186 return self._load_codereview_impl(
1187 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1188 **kwargs)
1189
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001190 def IsGerrit(self):
1191 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001192
1193 def GetCCList(self):
1194 """Return the users cc'd on this CL.
1195
agable92bec4f2016-08-24 09:27:27 -07001196 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001197 """
1198 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001199 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001200 more_cc = ','.join(self.watchers)
1201 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1202 return self.cc
1203
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001204 def GetCCListWithoutDefault(self):
1205 """Return the users cc'd on this CL excluding default ones."""
1206 if self.cc is None:
1207 self.cc = ','.join(self.watchers)
1208 return self.cc
1209
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001210 def SetWatchers(self, watchers):
1211 """Set the list of email addresses that should be cc'd based on the changed
1212 files in this CL.
1213 """
1214 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001215
1216 def GetBranch(self):
1217 """Returns the short branch name, e.g. 'master'."""
1218 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001219 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001220 if not branchref:
1221 return None
1222 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001223 self.branch = ShortBranchName(self.branchref)
1224 return self.branch
1225
1226 def GetBranchRef(self):
1227 """Returns the full branch name, e.g. 'refs/heads/master'."""
1228 self.GetBranch() # Poke the lazy loader.
1229 return self.branchref
1230
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001231 def ClearBranch(self):
1232 """Clears cached branch data of this object."""
1233 self.branch = self.branchref = None
1234
tandrii5d48c322016-08-18 16:19:37 -07001235 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1236 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1237 kwargs['branch'] = self.GetBranch()
1238 return _git_get_branch_config_value(key, default, **kwargs)
1239
1240 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1241 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1242 assert self.GetBranch(), (
1243 'this CL must have an associated branch to %sset %s%s' %
1244 ('un' if value is None else '',
1245 key,
1246 '' if value is None else ' to %r' % value))
1247 kwargs['branch'] = self.GetBranch()
1248 return _git_set_branch_config_value(key, value, **kwargs)
1249
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001250 @staticmethod
1251 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001252 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001253 e.g. 'origin', 'refs/heads/master'
1254 """
1255 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001256 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1257
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001258 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001259 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001260 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001261 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1262 error_ok=True).strip()
1263 if upstream_branch:
1264 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001265 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001266 # Else, try to guess the origin remote.
1267 remote_branches = RunGit(['branch', '-r']).split()
1268 if 'origin/master' in remote_branches:
1269 # Fall back on origin/master if it exits.
1270 remote = 'origin'
1271 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001272 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001273 DieWithError(
1274 'Unable to determine default branch to diff against.\n'
1275 'Either pass complete "git diff"-style arguments, like\n'
1276 ' git cl upload origin/master\n'
1277 'or verify this branch is set up to track another \n'
1278 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001279
1280 return remote, upstream_branch
1281
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001282 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001283 upstream_branch = self.GetUpstreamBranch()
1284 if not BranchExists(upstream_branch):
1285 DieWithError('The upstream for the current branch (%s) does not exist '
1286 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001287 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001288 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001289
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001290 def GetUpstreamBranch(self):
1291 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001292 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001293 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001294 upstream_branch = upstream_branch.replace('refs/heads/',
1295 'refs/remotes/%s/' % remote)
1296 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1297 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001298 self.upstream_branch = upstream_branch
1299 return self.upstream_branch
1300
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001301 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001302 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001303 remote, branch = None, self.GetBranch()
1304 seen_branches = set()
1305 while branch not in seen_branches:
1306 seen_branches.add(branch)
1307 remote, branch = self.FetchUpstreamTuple(branch)
1308 branch = ShortBranchName(branch)
1309 if remote != '.' or branch.startswith('refs/remotes'):
1310 break
1311 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001312 remotes = RunGit(['remote'], error_ok=True).split()
1313 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001314 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001315 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001316 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001317 logging.warn('Could not determine which remote this change is '
1318 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001319 else:
1320 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001321 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001322 branch = 'HEAD'
1323 if branch.startswith('refs/remotes'):
1324 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001325 elif branch.startswith('refs/branch-heads/'):
1326 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001327 else:
1328 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001329 return self._remote
1330
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001331 def GitSanityChecks(self, upstream_git_obj):
1332 """Checks git repo status and ensures diff is from local commits."""
1333
sbc@chromium.org79706062015-01-14 21:18:12 +00001334 if upstream_git_obj is None:
1335 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001336 print('ERROR: unable to determine current branch (detached HEAD?)',
1337 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001338 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001339 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001340 return False
1341
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001342 # Verify the commit we're diffing against is in our current branch.
1343 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1344 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1345 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001346 print('ERROR: %s is not in the current branch. You may need to rebase '
1347 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001348 return False
1349
1350 # List the commits inside the diff, and verify they are all local.
1351 commits_in_diff = RunGit(
1352 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1353 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1354 remote_branch = remote_branch.strip()
1355 if code != 0:
1356 _, remote_branch = self.GetRemoteBranch()
1357
1358 commits_in_remote = RunGit(
1359 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1360
1361 common_commits = set(commits_in_diff) & set(commits_in_remote)
1362 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001363 print('ERROR: Your diff contains %d commits already in %s.\n'
1364 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1365 'the diff. If you are using a custom git flow, you can override'
1366 ' the reference used for this check with "git config '
1367 'gitcl.remotebranch <git-ref>".' % (
1368 len(common_commits), remote_branch, upstream_git_obj),
1369 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001370 return False
1371 return True
1372
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001373 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001374 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001375
1376 Returns None if it is not set.
1377 """
tandrii5d48c322016-08-18 16:19:37 -07001378 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001379
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001380 def GetRemoteUrl(self):
1381 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1382
1383 Returns None if there is no remote.
1384 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001385 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001386 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1387
1388 # If URL is pointing to a local directory, it is probably a git cache.
1389 if os.path.isdir(url):
1390 url = RunGit(['config', 'remote.%s.url' % remote],
1391 error_ok=True,
1392 cwd=url).strip()
1393 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001394
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001395 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001396 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001397 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001398 self.issue = self._GitGetBranchConfigValue(
1399 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001400 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001401 return self.issue
1402
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001403 def GetIssueURL(self):
1404 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001405 issue = self.GetIssue()
1406 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001407 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001408 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001409
1410 def GetDescription(self, pretty=False):
1411 if not self.has_description:
1412 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001413 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001414 self.has_description = True
1415 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001416 # Set width to 72 columns + 2 space indent.
1417 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001418 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001419 lines = self.description.splitlines()
1420 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001421 return self.description
1422
1423 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001424 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001425 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001426 self.patchset = self._GitGetBranchConfigValue(
1427 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001428 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001429 return self.patchset
1430
1431 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001432 """Set this branch's patchset. If patchset=0, clears the patchset."""
1433 assert self.GetBranch()
1434 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001435 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001436 else:
1437 self.patchset = int(patchset)
1438 self._GitSetBranchConfigValue(
1439 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001440
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001441 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001442 """Set this branch's issue. If issue isn't given, clears the issue."""
1443 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001444 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001445 issue = int(issue)
1446 self._GitSetBranchConfigValue(
1447 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001448 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001449 codereview_server = self._codereview_impl.GetCodereviewServer()
1450 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001451 self._GitSetBranchConfigValue(
1452 self._codereview_impl.CodereviewServerConfigKey(),
1453 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001454 else:
tandrii5d48c322016-08-18 16:19:37 -07001455 # Reset all of these just to be clean.
1456 reset_suffixes = [
1457 'last-upload-hash',
1458 self._codereview_impl.IssueConfigKey(),
1459 self._codereview_impl.PatchsetConfigKey(),
1460 self._codereview_impl.CodereviewServerConfigKey(),
1461 ] + self._PostUnsetIssueProperties()
1462 for prop in reset_suffixes:
1463 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001464 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001465 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001466
dnjba1b0f32016-09-02 12:37:42 -07001467 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001468 if not self.GitSanityChecks(upstream_branch):
1469 DieWithError('\nGit sanity check failure')
1470
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001471 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001472 if not root:
1473 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001474 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001475
1476 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001477 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001478 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001479 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001480 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001481 except subprocess2.CalledProcessError:
1482 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001483 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001484 'This branch probably doesn\'t exist anymore. To reset the\n'
1485 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001486 ' git branch --set-upstream-to origin/master %s\n'
1487 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001488 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001489
maruel@chromium.org52424302012-08-29 15:14:30 +00001490 issue = self.GetIssue()
1491 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001492 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001493 description = self.GetDescription()
1494 else:
1495 # If the change was never uploaded, use the log messages of all commits
1496 # up to the branch point, as git cl upload will prefill the description
1497 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001498 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1499 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001500
1501 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001502 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001503 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001504 name,
1505 description,
1506 absroot,
1507 files,
1508 issue,
1509 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001510 author,
1511 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001512
dsansomee2d6fd92016-09-08 00:10:47 -07001513 def UpdateDescription(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001514 self.description = description
dsansomee2d6fd92016-09-08 00:10:47 -07001515 return self._codereview_impl.UpdateDescriptionRemote(
1516 description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001517
1518 def RunHook(self, committing, may_prompt, verbose, change):
1519 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1520 try:
1521 return presubmit_support.DoPresubmitChecks(change, committing,
1522 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1523 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001524 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1525 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001526 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001527 DieWithError(
1528 ('%s\nMaybe your depot_tools is out of date?\n'
1529 'If all fails, contact maruel@') % e)
1530
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001531 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1532 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001533 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1534 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001535 else:
1536 # Assume url.
1537 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1538 urlparse.urlparse(issue_arg))
1539 if not parsed_issue_arg or not parsed_issue_arg.valid:
1540 DieWithError('Failed to parse issue argument "%s". '
1541 'Must be an issue number or a valid URL.' % issue_arg)
1542 return self._codereview_impl.CMDPatchWithParsedIssue(
1543 parsed_issue_arg, reject, nocommit, directory)
1544
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001545 def CMDUpload(self, options, git_diff_args, orig_args):
1546 """Uploads a change to codereview."""
1547 if git_diff_args:
1548 # TODO(ukai): is it ok for gerrit case?
1549 base_branch = git_diff_args[0]
1550 else:
1551 if self.GetBranch() is None:
1552 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1553
1554 # Default to diffing against common ancestor of upstream branch
1555 base_branch = self.GetCommonAncestorWithUpstream()
1556 git_diff_args = [base_branch, 'HEAD']
1557
1558 # Make sure authenticated to codereview before running potentially expensive
1559 # hooks. It is a fast, best efforts check. Codereview still can reject the
1560 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001561 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001562
1563 # Apply watchlists on upload.
1564 change = self.GetChange(base_branch, None)
1565 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1566 files = [f.LocalPath() for f in change.AffectedFiles()]
1567 if not options.bypass_watchlists:
1568 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1569
1570 if not options.bypass_hooks:
1571 if options.reviewers or options.tbr_owners:
1572 # Set the reviewer list now so that presubmit checks can access it.
1573 change_description = ChangeDescription(change.FullDescriptionText())
1574 change_description.update_reviewers(options.reviewers,
1575 options.tbr_owners,
1576 change)
1577 change.SetDescriptionText(change_description.description)
1578 hook_results = self.RunHook(committing=False,
1579 may_prompt=not options.force,
1580 verbose=options.verbose,
1581 change=change)
1582 if not hook_results.should_continue():
1583 return 1
1584 if not options.reviewers and hook_results.reviewers:
1585 options.reviewers = hook_results.reviewers.split(',')
1586
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001587 # TODO(tandrii): Checking local patchset against remote patchset is only
1588 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1589 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001590 latest_patchset = self.GetMostRecentPatchset()
1591 local_patchset = self.GetPatchset()
1592 if (latest_patchset and local_patchset and
1593 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001594 print('The last upload made from this repository was patchset #%d but '
1595 'the most recent patchset on the server is #%d.'
1596 % (local_patchset, latest_patchset))
1597 print('Uploading will still work, but if you\'ve uploaded to this '
1598 'issue from another machine or branch the patch you\'re '
1599 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001600 ask_for_data('About to upload; enter to confirm.')
1601
1602 print_stats(options.similarity, options.find_copies, git_diff_args)
1603 ret = self.CMDUploadChange(options, git_diff_args, change)
1604 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001605 if options.use_commit_queue:
1606 self.SetCQState(_CQState.COMMIT)
1607 elif options.cq_dry_run:
1608 self.SetCQState(_CQState.DRY_RUN)
1609
tandrii5d48c322016-08-18 16:19:37 -07001610 _git_set_branch_config_value('last-upload-hash',
1611 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001612 # Run post upload hooks, if specified.
1613 if settings.GetRunPostUploadHook():
1614 presubmit_support.DoPostUploadExecuter(
1615 change,
1616 self,
1617 settings.GetRoot(),
1618 options.verbose,
1619 sys.stdout)
1620
1621 # Upload all dependencies if specified.
1622 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001623 print()
1624 print('--dependencies has been specified.')
1625 print('All dependent local branches will be re-uploaded.')
1626 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001627 # Remove the dependencies flag from args so that we do not end up in a
1628 # loop.
1629 orig_args.remove('--dependencies')
1630 ret = upload_branch_deps(self, orig_args)
1631 return ret
1632
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001633 def SetCQState(self, new_state):
1634 """Update the CQ state for latest patchset.
1635
1636 Issue must have been already uploaded and known.
1637 """
1638 assert new_state in _CQState.ALL_STATES
1639 assert self.GetIssue()
1640 return self._codereview_impl.SetCQState(new_state)
1641
qyearsley1fdfcb62016-10-24 13:22:03 -07001642 def TriggerDryRun(self):
1643 """Triggers a dry run and prints a warning on failure."""
1644 # TODO(qyearsley): Either re-use this method in CMDset_commit
1645 # and CMDupload, or change CMDtry to trigger dry runs with
1646 # just SetCQState, and catch keyboard interrupt and other
1647 # errors in that method.
1648 try:
1649 self.SetCQState(_CQState.DRY_RUN)
1650 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1651 return 0
1652 except KeyboardInterrupt:
1653 raise
1654 except:
1655 print('WARNING: failed to trigger CQ Dry Run.\n'
1656 'Either:\n'
1657 ' * your project has no CQ\n'
1658 ' * you don\'t have permission to trigger Dry Run\n'
1659 ' * bug in this code (see stack trace below).\n'
1660 'Consider specifying which bots to trigger manually '
1661 'or asking your project owners for permissions '
1662 'or contacting Chrome Infrastructure team at '
1663 'https://www.chromium.org/infra\n\n')
1664 # Still raise exception so that stack trace is printed.
1665 raise
1666
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001667 # Forward methods to codereview specific implementation.
1668
1669 def CloseIssue(self):
1670 return self._codereview_impl.CloseIssue()
1671
1672 def GetStatus(self):
1673 return self._codereview_impl.GetStatus()
1674
1675 def GetCodereviewServer(self):
1676 return self._codereview_impl.GetCodereviewServer()
1677
tandriide281ae2016-10-12 06:02:30 -07001678 def GetIssueOwner(self):
1679 """Get owner from codereview, which may differ from this checkout."""
1680 return self._codereview_impl.GetIssueOwner()
1681
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001682 def GetApprovingReviewers(self):
1683 return self._codereview_impl.GetApprovingReviewers()
1684
1685 def GetMostRecentPatchset(self):
1686 return self._codereview_impl.GetMostRecentPatchset()
1687
tandriide281ae2016-10-12 06:02:30 -07001688 def CannotTriggerTryJobReason(self):
1689 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1690 return self._codereview_impl.CannotTriggerTryJobReason()
1691
tandrii8c5a3532016-11-04 07:52:02 -07001692 def GetTryjobProperties(self, patchset=None):
1693 """Returns dictionary of properties to launch tryjob."""
1694 return self._codereview_impl.GetTryjobProperties(patchset=patchset)
1695
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001696 def __getattr__(self, attr):
1697 # This is because lots of untested code accesses Rietveld-specific stuff
1698 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001699 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001700 # Note that child method defines __getattr__ as well, and forwards it here,
1701 # because _RietveldChangelistImpl is not cleaned up yet, and given
1702 # deprecation of Rietveld, it should probably be just removed.
1703 # Until that time, avoid infinite recursion by bypassing __getattr__
1704 # of implementation class.
1705 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001706
1707
1708class _ChangelistCodereviewBase(object):
1709 """Abstract base class encapsulating codereview specifics of a changelist."""
1710 def __init__(self, changelist):
1711 self._changelist = changelist # instance of Changelist
1712
1713 def __getattr__(self, attr):
1714 # Forward methods to changelist.
1715 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1716 # _RietveldChangelistImpl to avoid this hack?
1717 return getattr(self._changelist, attr)
1718
1719 def GetStatus(self):
1720 """Apply a rough heuristic to give a simple summary of an issue's review
1721 or CQ status, assuming adherence to a common workflow.
1722
1723 Returns None if no issue for this branch, or specific string keywords.
1724 """
1725 raise NotImplementedError()
1726
1727 def GetCodereviewServer(self):
1728 """Returns server URL without end slash, like "https://codereview.com"."""
1729 raise NotImplementedError()
1730
1731 def FetchDescription(self):
1732 """Fetches and returns description from the codereview server."""
1733 raise NotImplementedError()
1734
tandrii5d48c322016-08-18 16:19:37 -07001735 @classmethod
1736 def IssueConfigKey(cls):
1737 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001738 raise NotImplementedError()
1739
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001740 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001741 def PatchsetConfigKey(cls):
1742 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001743 raise NotImplementedError()
1744
tandrii5d48c322016-08-18 16:19:37 -07001745 @classmethod
1746 def CodereviewServerConfigKey(cls):
1747 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001748 raise NotImplementedError()
1749
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001750 def _PostUnsetIssueProperties(self):
1751 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001752 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001753
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001754 def GetRieveldObjForPresubmit(self):
1755 # This is an unfortunate Rietveld-embeddedness in presubmit.
1756 # For non-Rietveld codereviews, this probably should return a dummy object.
1757 raise NotImplementedError()
1758
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001759 def GetGerritObjForPresubmit(self):
1760 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1761 return None
1762
dsansomee2d6fd92016-09-08 00:10:47 -07001763 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001764 """Update the description on codereview site."""
1765 raise NotImplementedError()
1766
1767 def CloseIssue(self):
1768 """Closes the issue."""
1769 raise NotImplementedError()
1770
1771 def GetApprovingReviewers(self):
1772 """Returns a list of reviewers approving the change.
1773
1774 Note: not necessarily committers.
1775 """
1776 raise NotImplementedError()
1777
1778 def GetMostRecentPatchset(self):
1779 """Returns the most recent patchset number from the codereview site."""
1780 raise NotImplementedError()
1781
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001782 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1783 directory):
1784 """Fetches and applies the issue.
1785
1786 Arguments:
1787 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1788 reject: if True, reject the failed patch instead of switching to 3-way
1789 merge. Rietveld only.
1790 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1791 only.
1792 directory: switch to directory before applying the patch. Rietveld only.
1793 """
1794 raise NotImplementedError()
1795
1796 @staticmethod
1797 def ParseIssueURL(parsed_url):
1798 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1799 failed."""
1800 raise NotImplementedError()
1801
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001802 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001803 """Best effort check that user is authenticated with codereview server.
1804
1805 Arguments:
1806 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001807 refresh: whether to attempt to refresh credentials. Ignored if not
1808 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001809 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001810 raise NotImplementedError()
1811
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001812 def CMDUploadChange(self, options, args, change):
1813 """Uploads a change to codereview."""
1814 raise NotImplementedError()
1815
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001816 def SetCQState(self, new_state):
1817 """Update the CQ state for latest patchset.
1818
1819 Issue must have been already uploaded and known.
1820 """
1821 raise NotImplementedError()
1822
tandriie113dfd2016-10-11 10:20:12 -07001823 def CannotTriggerTryJobReason(self):
1824 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1825 raise NotImplementedError()
1826
tandriide281ae2016-10-12 06:02:30 -07001827 def GetIssueOwner(self):
1828 raise NotImplementedError()
1829
tandrii8c5a3532016-11-04 07:52:02 -07001830 def GetTryjobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001831 raise NotImplementedError()
1832
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001833
1834class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1835 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1836 super(_RietveldChangelistImpl, self).__init__(changelist)
1837 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001838 if not rietveld_server:
1839 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001840
1841 self._rietveld_server = rietveld_server
1842 self._auth_config = auth_config
1843 self._props = None
1844 self._rpc_server = None
1845
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001846 def GetCodereviewServer(self):
1847 if not self._rietveld_server:
1848 # If we're on a branch then get the server potentially associated
1849 # with that branch.
1850 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001851 self._rietveld_server = gclient_utils.UpgradeToHttps(
1852 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001853 if not self._rietveld_server:
1854 self._rietveld_server = settings.GetDefaultServerUrl()
1855 return self._rietveld_server
1856
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001857 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001858 """Best effort check that user is authenticated with Rietveld server."""
1859 if self._auth_config.use_oauth2:
1860 authenticator = auth.get_authenticator_for_host(
1861 self.GetCodereviewServer(), self._auth_config)
1862 if not authenticator.has_cached_credentials():
1863 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001864 if refresh:
1865 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001866
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001867 def FetchDescription(self):
1868 issue = self.GetIssue()
1869 assert issue
1870 try:
1871 return self.RpcServer().get_description(issue).strip()
1872 except urllib2.HTTPError as e:
1873 if e.code == 404:
1874 DieWithError(
1875 ('\nWhile fetching the description for issue %d, received a '
1876 '404 (not found)\n'
1877 'error. It is likely that you deleted this '
1878 'issue on the server. If this is the\n'
1879 'case, please run\n\n'
1880 ' git cl issue 0\n\n'
1881 'to clear the association with the deleted issue. Then run '
1882 'this command again.') % issue)
1883 else:
1884 DieWithError(
1885 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1886 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001887 print('Warning: Failed to retrieve CL description due to network '
1888 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001889 return ''
1890
1891 def GetMostRecentPatchset(self):
1892 return self.GetIssueProperties()['patchsets'][-1]
1893
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001894 def GetIssueProperties(self):
1895 if self._props is None:
1896 issue = self.GetIssue()
1897 if not issue:
1898 self._props = {}
1899 else:
1900 self._props = self.RpcServer().get_issue_properties(issue, True)
1901 return self._props
1902
tandriie113dfd2016-10-11 10:20:12 -07001903 def CannotTriggerTryJobReason(self):
1904 props = self.GetIssueProperties()
1905 if not props:
1906 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1907 if props.get('closed'):
1908 return 'CL %s is closed' % self.GetIssue()
1909 if props.get('private'):
1910 return 'CL %s is private' % self.GetIssue()
1911 return None
1912
tandrii8c5a3532016-11-04 07:52:02 -07001913 def GetTryjobProperties(self, patchset=None):
1914 """Returns dictionary of properties to launch tryjob."""
1915 project = (self.GetIssueProperties() or {}).get('project')
1916 return {
1917 'issue': self.GetIssue(),
1918 'patch_project': project,
1919 'patch_storage': 'rietveld',
1920 'patchset': patchset or self.GetPatchset(),
1921 'rietveld': self.GetCodereviewServer(),
1922 }
1923
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001924 def GetApprovingReviewers(self):
1925 return get_approving_reviewers(self.GetIssueProperties())
1926
tandriide281ae2016-10-12 06:02:30 -07001927 def GetIssueOwner(self):
1928 return (self.GetIssueProperties() or {}).get('owner_email')
1929
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001930 def AddComment(self, message):
1931 return self.RpcServer().add_comment(self.GetIssue(), message)
1932
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001933 def GetStatus(self):
1934 """Apply a rough heuristic to give a simple summary of an issue's review
1935 or CQ status, assuming adherence to a common workflow.
1936
1937 Returns None if no issue for this branch, or one of the following keywords:
1938 * 'error' - error from review tool (including deleted issues)
1939 * 'unsent' - not sent for review
1940 * 'waiting' - waiting for review
1941 * 'reply' - waiting for owner to reply to review
1942 * 'lgtm' - LGTM from at least one approved reviewer
1943 * 'commit' - in the commit queue
1944 * 'closed' - closed
1945 """
1946 if not self.GetIssue():
1947 return None
1948
1949 try:
1950 props = self.GetIssueProperties()
1951 except urllib2.HTTPError:
1952 return 'error'
1953
1954 if props.get('closed'):
1955 # Issue is closed.
1956 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001957 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001958 # Issue is in the commit queue.
1959 return 'commit'
1960
1961 try:
1962 reviewers = self.GetApprovingReviewers()
1963 except urllib2.HTTPError:
1964 return 'error'
1965
1966 if reviewers:
1967 # Was LGTM'ed.
1968 return 'lgtm'
1969
1970 messages = props.get('messages') or []
1971
tandrii9d2c7a32016-06-22 03:42:45 -07001972 # Skip CQ messages that don't require owner's action.
1973 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1974 if 'Dry run:' in messages[-1]['text']:
1975 messages.pop()
1976 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1977 # This message always follows prior messages from CQ,
1978 # so skip this too.
1979 messages.pop()
1980 else:
1981 # This is probably a CQ messages warranting user attention.
1982 break
1983
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001984 if not messages:
1985 # No message was sent.
1986 return 'unsent'
1987 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001988 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001989 return 'reply'
1990 return 'waiting'
1991
dsansomee2d6fd92016-09-08 00:10:47 -07001992 def UpdateDescriptionRemote(self, description, force=False):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001993 return self.RpcServer().update_description(
1994 self.GetIssue(), self.description)
1995
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001996 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001997 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001998
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001999 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002000 return self.SetFlags({flag: value})
2001
2002 def SetFlags(self, flags):
2003 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002004 """
phajdan.jr68598232016-08-10 03:28:28 -07002005 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002006 try:
tandrii4b233bd2016-07-06 03:50:29 -07002007 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002008 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002009 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002010 if e.code == 404:
2011 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2012 if e.code == 403:
2013 DieWithError(
2014 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002015 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002016 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002017
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002018 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002019 """Returns an upload.RpcServer() to access this review's rietveld instance.
2020 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002021 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002022 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002023 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002024 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002025 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002026
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002027 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002028 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002029 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002030
tandrii5d48c322016-08-18 16:19:37 -07002031 @classmethod
2032 def PatchsetConfigKey(cls):
2033 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002034
tandrii5d48c322016-08-18 16:19:37 -07002035 @classmethod
2036 def CodereviewServerConfigKey(cls):
2037 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002038
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002039 def GetRieveldObjForPresubmit(self):
2040 return self.RpcServer()
2041
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002042 def SetCQState(self, new_state):
2043 props = self.GetIssueProperties()
2044 if props.get('private'):
2045 DieWithError('Cannot set-commit on private issue')
2046
2047 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002048 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002049 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002050 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002051 else:
tandrii4b233bd2016-07-06 03:50:29 -07002052 assert new_state == _CQState.DRY_RUN
2053 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002054
2055
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002056 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2057 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002058 # PatchIssue should never be called with a dirty tree. It is up to the
2059 # caller to check this, but just in case we assert here since the
2060 # consequences of the caller not checking this could be dire.
2061 assert(not git_common.is_dirty_git_tree('apply'))
2062 assert(parsed_issue_arg.valid)
2063 self._changelist.issue = parsed_issue_arg.issue
2064 if parsed_issue_arg.hostname:
2065 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2066
skobes6468b902016-10-24 08:45:10 -07002067 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2068 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2069 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002070 try:
skobes6468b902016-10-24 08:45:10 -07002071 scm_obj.apply_patch(patchset_object)
2072 except Exception as e:
2073 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002074 return 1
2075
2076 # If we had an issue, commit the current state and register the issue.
2077 if not nocommit:
2078 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2079 'patch from issue %(i)s at patchset '
2080 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2081 % {'i': self.GetIssue(), 'p': patchset})])
2082 self.SetIssue(self.GetIssue())
2083 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07002084 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002085 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002086 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002087 return 0
2088
2089 @staticmethod
2090 def ParseIssueURL(parsed_url):
2091 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2092 return None
wychen3c1c1722016-08-04 11:46:36 -07002093 # Rietveld patch: https://domain/<number>/#ps<patchset>
2094 match = re.match(r'/(\d+)/$', parsed_url.path)
2095 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2096 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002097 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002098 issue=int(match.group(1)),
2099 patchset=int(match2.group(1)),
2100 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002101 # Typical url: https://domain/<issue_number>[/[other]]
2102 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2103 if match:
skobes6468b902016-10-24 08:45:10 -07002104 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002105 issue=int(match.group(1)),
2106 hostname=parsed_url.netloc)
2107 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2108 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2109 if match:
skobes6468b902016-10-24 08:45:10 -07002110 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002111 issue=int(match.group(1)),
2112 patchset=int(match.group(2)),
skobes6468b902016-10-24 08:45:10 -07002113 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002114 return None
2115
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002116 def CMDUploadChange(self, options, args, change):
2117 """Upload the patch to Rietveld."""
2118 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2119 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002120 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2121 if options.emulate_svn_auto_props:
2122 upload_args.append('--emulate_svn_auto_props')
2123
2124 change_desc = None
2125
2126 if options.email is not None:
2127 upload_args.extend(['--email', options.email])
2128
2129 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002130 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002131 upload_args.extend(['--title', options.title])
2132 if options.message:
2133 upload_args.extend(['--message', options.message])
2134 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002135 print('This branch is associated with issue %s. '
2136 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002137 else:
nodirca166002016-06-27 10:59:51 -07002138 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002139 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002140 if options.message:
2141 message = options.message
2142 else:
2143 message = CreateDescriptionFromLog(args)
2144 if options.title:
2145 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002146 change_desc = ChangeDescription(message)
2147 if options.reviewers or options.tbr_owners:
2148 change_desc.update_reviewers(options.reviewers,
2149 options.tbr_owners,
2150 change)
2151 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002152 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002153
2154 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002155 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002156 return 1
2157
2158 upload_args.extend(['--message', change_desc.description])
2159 if change_desc.get_reviewers():
2160 upload_args.append('--reviewers=%s' % ','.join(
2161 change_desc.get_reviewers()))
2162 if options.send_mail:
2163 if not change_desc.get_reviewers():
2164 DieWithError("Must specify reviewers to send email.")
2165 upload_args.append('--send_mail')
2166
2167 # We check this before applying rietveld.private assuming that in
2168 # rietveld.cc only addresses which we can send private CLs to are listed
2169 # if rietveld.private is set, and so we should ignore rietveld.cc only
2170 # when --private is specified explicitly on the command line.
2171 if options.private:
2172 logging.warn('rietveld.cc is ignored since private flag is specified. '
2173 'You need to review and add them manually if necessary.')
2174 cc = self.GetCCListWithoutDefault()
2175 else:
2176 cc = self.GetCCList()
2177 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002178 if change_desc.get_cced():
2179 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002180 if cc:
2181 upload_args.extend(['--cc', cc])
2182
2183 if options.private or settings.GetDefaultPrivateFlag() == "True":
2184 upload_args.append('--private')
2185
2186 upload_args.extend(['--git_similarity', str(options.similarity)])
2187 if not options.find_copies:
2188 upload_args.extend(['--git_no_find_copies'])
2189
2190 # Include the upstream repo's URL in the change -- this is useful for
2191 # projects that have their source spread across multiple repos.
2192 remote_url = self.GetGitBaseUrlFromConfig()
2193 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002194 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2195 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2196 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002197 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002198 remote, remote_branch = self.GetRemoteBranch()
2199 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01002200 pending_prefix_check=True,
2201 remote_url=self.GetRemoteUrl())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002202 if target_ref:
2203 upload_args.extend(['--target_ref', target_ref])
2204
2205 # Look for dependent patchsets. See crbug.com/480453 for more details.
2206 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2207 upstream_branch = ShortBranchName(upstream_branch)
2208 if remote is '.':
2209 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002210 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002211 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002212 print()
2213 print('Skipping dependency patchset upload because git config '
2214 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2215 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002216 else:
2217 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002218 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002219 auth_config=auth_config)
2220 branch_cl_issue_url = branch_cl.GetIssueURL()
2221 branch_cl_issue = branch_cl.GetIssue()
2222 branch_cl_patchset = branch_cl.GetPatchset()
2223 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2224 upload_args.extend(
2225 ['--depends_on_patchset', '%s:%s' % (
2226 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002227 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002228 '\n'
2229 'The current branch (%s) is tracking a local branch (%s) with '
2230 'an associated CL.\n'
2231 'Adding %s/#ps%s as a dependency patchset.\n'
2232 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2233 branch_cl_patchset))
2234
2235 project = settings.GetProject()
2236 if project:
2237 upload_args.extend(['--project', project])
2238
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002239 try:
2240 upload_args = ['upload'] + upload_args + args
2241 logging.info('upload.RealMain(%s)', upload_args)
2242 issue, patchset = upload.RealMain(upload_args)
2243 issue = int(issue)
2244 patchset = int(patchset)
2245 except KeyboardInterrupt:
2246 sys.exit(1)
2247 except:
2248 # If we got an exception after the user typed a description for their
2249 # change, back up the description before re-raising.
2250 if change_desc:
2251 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2252 print('\nGot exception while uploading -- saving description to %s\n' %
2253 backup_path)
2254 backup_file = open(backup_path, 'w')
2255 backup_file.write(change_desc.description)
2256 backup_file.close()
2257 raise
2258
2259 if not self.GetIssue():
2260 self.SetIssue(issue)
2261 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002262 return 0
2263
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002264
2265class _GerritChangelistImpl(_ChangelistCodereviewBase):
2266 def __init__(self, changelist, auth_config=None):
2267 # auth_config is Rietveld thing, kept here to preserve interface only.
2268 super(_GerritChangelistImpl, self).__init__(changelist)
2269 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002270 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002271 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002272 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002273 # Map from change number (issue) to its detail cache.
2274 self._detail_cache = {}
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
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002324 def EnsureAuthenticated(self, force, refresh=None):
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
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002492 def _GetChangeDetail(self, options=None, issue=None,
2493 no_cache=False):
2494 """Returns details of the issue by querying Gerrit and caching results.
2495
2496 If fresh data is needed, set no_cache=True which will clear cache and
2497 thus new data will be fetched from Gerrit.
2498 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002499 options = options or []
2500 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002501 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002502
2503 # Normalize issue and options for consistent keys in cache.
2504 issue = str(issue)
2505 options = [o.upper() for o in options]
2506
2507 # Check in cache first unless no_cache is True.
2508 if no_cache:
2509 self._detail_cache.pop(issue, None)
2510 else:
2511 options_set = frozenset(options)
2512 for cached_options_set, data in self._detail_cache.get(issue, []):
2513 # Assumption: data fetched before with extra options is suitable
2514 # for return for a smaller set of options.
2515 # For example, if we cached data for
2516 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2517 # and request is for options=[CURRENT_REVISION],
2518 # THEN we can return prior cached data.
2519 if options_set.issubset(cached_options_set):
2520 return data
2521
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002522 try:
2523 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2524 options, ignore_404=False)
2525 except gerrit_util.GerritError as e:
2526 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002527 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002528 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002529
2530 self._detail_cache.setdefault(issue, []).append((frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002531 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002532
agable32978d92016-11-01 12:55:02 -07002533 def _GetChangeCommit(self, issue=None):
2534 issue = issue or self.GetIssue()
2535 assert issue, 'issue is required to query Gerrit'
2536 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2537 if not data:
Aaron Gablea45ee112016-11-22 15:14:38 -08002538 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
agable32978d92016-11-01 12:55:02 -07002539 return data
2540
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002541 def CMDLand(self, force, bypass_hooks, verbose):
2542 if git_common.is_dirty_git_tree('land'):
2543 return 1
tandriid60367b2016-06-22 05:25:12 -07002544 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2545 if u'Commit-Queue' in detail.get('labels', {}):
2546 if not force:
2547 ask_for_data('\nIt seems this repository has a Commit Queue, '
2548 'which can test and land changes for you. '
2549 'Are you sure you wish to bypass it?\n'
2550 'Press Enter to continue, Ctrl+C to abort.')
2551
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002552 differs = True
tandriic4344b52016-08-29 06:04:54 -07002553 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002554 # Note: git diff outputs nothing if there is no diff.
2555 if not last_upload or RunGit(['diff', last_upload]).strip():
2556 print('WARNING: some changes from local branch haven\'t been uploaded')
2557 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002558 if detail['current_revision'] == last_upload:
2559 differs = False
2560 else:
2561 print('WARNING: local branch contents differ from latest uploaded '
2562 'patchset')
2563 if differs:
2564 if not force:
2565 ask_for_data(
Andrii Shyshkalov3aac7752016-11-16 15:23:13 +01002566 'Do you want to submit latest Gerrit patchset and bypass hooks?\n'
2567 'Press Enter to continue, Ctrl+C to abort.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002568 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2569 elif not bypass_hooks:
2570 hook_results = self.RunHook(
2571 committing=True,
2572 may_prompt=not force,
2573 verbose=verbose,
2574 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2575 if not hook_results.should_continue():
2576 return 1
2577
2578 self.SubmitIssue(wait_for_merge=True)
2579 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002580 links = self._GetChangeCommit().get('web_links', [])
2581 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002582 if link.get('name') == 'gitiles' and link.get('url'):
agable32978d92016-11-01 12:55:02 -07002583 print('Landed as %s' % link.get('url'))
2584 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002585 return 0
2586
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002587 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2588 directory):
2589 assert not reject
2590 assert not nocommit
2591 assert not directory
2592 assert parsed_issue_arg.valid
2593
2594 self._changelist.issue = parsed_issue_arg.issue
2595
2596 if parsed_issue_arg.hostname:
2597 self._gerrit_host = parsed_issue_arg.hostname
2598 self._gerrit_server = 'https://%s' % self._gerrit_host
2599
tandriic2405f52016-10-10 08:13:15 -07002600 try:
2601 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002602 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002603 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002604
2605 if not parsed_issue_arg.patchset:
2606 # Use current revision by default.
2607 revision_info = detail['revisions'][detail['current_revision']]
2608 patchset = int(revision_info['_number'])
2609 else:
2610 patchset = parsed_issue_arg.patchset
2611 for revision_info in detail['revisions'].itervalues():
2612 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2613 break
2614 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002615 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002616 (parsed_issue_arg.patchset, self.GetIssue()))
2617
2618 fetch_info = revision_info['fetch']['http']
2619 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2620 RunGit(['cherry-pick', 'FETCH_HEAD'])
2621 self.SetIssue(self.GetIssue())
2622 self.SetPatchset(patchset)
Aaron Gablea45ee112016-11-22 15:14:38 -08002623 print('Committed patch for change %i patchset %i locally' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002624 (self.GetIssue(), self.GetPatchset()))
2625 return 0
2626
2627 @staticmethod
2628 def ParseIssueURL(parsed_url):
2629 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2630 return None
2631 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2632 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2633 # Short urls like https://domain/<issue_number> can be used, but don't allow
2634 # specifying the patchset (you'd 404), but we allow that here.
2635 if parsed_url.path == '/':
2636 part = parsed_url.fragment
2637 else:
2638 part = parsed_url.path
2639 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2640 if match:
2641 return _ParsedIssueNumberArgument(
2642 issue=int(match.group(2)),
2643 patchset=int(match.group(4)) if match.group(4) else None,
2644 hostname=parsed_url.netloc)
2645 return None
2646
tandrii16e0b4e2016-06-07 10:34:28 -07002647 def _GerritCommitMsgHookCheck(self, offer_removal):
2648 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2649 if not os.path.exists(hook):
2650 return
2651 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2652 # custom developer made one.
2653 data = gclient_utils.FileRead(hook)
2654 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2655 return
2656 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002657 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002658 'and may interfere with it in subtle ways.\n'
2659 'We recommend you remove the commit-msg hook.')
2660 if offer_removal:
2661 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2662 if reply.lower().startswith('y'):
2663 gclient_utils.rm_file_or_tree(hook)
2664 print('Gerrit commit-msg hook removed.')
2665 else:
2666 print('OK, will keep Gerrit commit-msg hook in place.')
2667
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002668 def CMDUploadChange(self, options, args, change):
2669 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002670 if options.squash and options.no_squash:
2671 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002672
2673 if not options.squash and not options.no_squash:
2674 # Load default for user, repo, squash=true, in this order.
2675 options.squash = settings.GetSquashGerritUploads()
2676 elif options.no_squash:
2677 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002678
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002679 # We assume the remote called "origin" is the one we want.
2680 # It is probably not worthwhile to support different workflows.
2681 gerrit_remote = 'origin'
2682
2683 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01002684 # Gerrit will not support pending prefix at all.
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002685 branch = GetTargetRef(remote, remote_branch, options.target_branch,
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01002686 pending_prefix_check=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002687
Aaron Gableb56ad332017-01-06 15:24:31 -08002688 # This may be None; default fallback value is determined in logic below.
2689 title = options.title
Aaron Gable856585d2017-01-23 15:32:00 -08002690 automatic_title = False
Aaron Gableb56ad332017-01-06 15:24:31 -08002691
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002692 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002693 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002694 if self.GetIssue():
2695 # Try to get the message from a previous upload.
2696 message = self.GetDescription()
2697 if not message:
2698 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002699 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002700 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002701 if not title:
2702 default_title = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
2703 title = ask_for_data(
2704 'Title for patchset [%s]: ' % default_title) or default_title
Aaron Gable856585d2017-01-23 15:32:00 -08002705 if title == default_title:
2706 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002707 change_id = self._GetChangeDetail()['change_id']
2708 while True:
2709 footer_change_ids = git_footers.get_footer_change_id(message)
2710 if footer_change_ids == [change_id]:
2711 break
2712 if not footer_change_ids:
2713 message = git_footers.add_footer_change_id(message, change_id)
Aaron Gablea45ee112016-11-22 15:14:38 -08002714 print('WARNING: appended missing Change-Id to change description')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002715 continue
2716 # There is already a valid footer but with different or several ids.
2717 # Doing this automatically is non-trivial as we don't want to lose
2718 # existing other footers, yet we want to append just 1 desired
2719 # Change-Id. Thus, just create a new footer, but let user verify the
2720 # new description.
2721 message = '%s\n\nChange-Id: %s' % (message, change_id)
2722 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002723 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002724 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002725 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002726 'Please, check the proposed correction to the description, '
2727 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2728 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2729 change_id))
2730 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2731 if not options.force:
2732 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002733 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002734 message = change_desc.description
2735 if not message:
2736 DieWithError("Description is empty. Aborting...")
2737 # Continue the while loop.
2738 # Sanity check of this code - we should end up with proper message
2739 # footer.
2740 assert [change_id] == git_footers.get_footer_change_id(message)
2741 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002742 else: # if not self.GetIssue()
2743 if options.message:
2744 message = options.message
2745 else:
2746 message = CreateDescriptionFromLog(args)
2747 if options.title:
2748 message = options.title + '\n\n' + message
2749 change_desc = ChangeDescription(message)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002750 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002751 change_desc.prompt(bug=options.bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002752 # On first upload, patchset title is always this string, while
2753 # --title flag gets converted to first line of message.
2754 title = 'Initial upload'
Aaron Gable856585d2017-01-23 15:32:00 -08002755 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002756 if not change_desc.description:
2757 DieWithError("Description is empty. Aborting...")
2758 message = change_desc.description
2759 change_ids = git_footers.get_footer_change_id(message)
2760 if len(change_ids) > 1:
2761 DieWithError('too many Change-Id footers, at most 1 allowed.')
2762 if not change_ids:
2763 # Generate the Change-Id automatically.
2764 message = git_footers.add_footer_change_id(
2765 message, GenerateGerritChangeId(message))
2766 change_desc.set_description(message)
2767 change_ids = git_footers.get_footer_change_id(message)
2768 assert len(change_ids) == 1
2769 change_id = change_ids[0]
2770
2771 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2772 if remote is '.':
2773 # If our upstream branch is local, we base our squashed commit on its
2774 # squashed version.
2775 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2776 # Check the squashed hash of the parent.
2777 parent = RunGit(['config',
2778 'branch.%s.gerritsquashhash' % upstream_branch_name],
2779 error_ok=True).strip()
2780 # Verify that the upstream branch has been uploaded too, otherwise
2781 # Gerrit will create additional CLs when uploading.
2782 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2783 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002784 DieWithError(
Aaron Gable48746652017-01-06 11:49:21 -08002785 '\nUpload upstream branch %s first.\n'
2786 'It is likely that this branch has been rebased since its last '
2787 'upload, so you just need to upload it again.\n'
2788 '(If you uploaded it with --no-squash, then branch dependencies '
2789 'are not supported, and you should reupload with --squash.)'
2790 % upstream_branch_name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002791 else:
2792 parent = self.GetCommonAncestorWithUpstream()
2793
2794 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2795 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2796 '-m', message]).strip()
2797 else:
2798 change_desc = ChangeDescription(
2799 options.message or CreateDescriptionFromLog(args))
2800 if not change_desc.description:
2801 DieWithError("Description is empty. Aborting...")
2802
2803 if not git_footers.get_footer_change_id(change_desc.description):
2804 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002805 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2806 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002807 ref_to_push = 'HEAD'
2808 parent = '%s/%s' % (gerrit_remote, branch)
2809 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2810
2811 assert change_desc
2812 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2813 ref_to_push)]).splitlines()
2814 if len(commits) > 1:
2815 print('WARNING: This will upload %d commits. Run the following command '
2816 'to see which commits will be uploaded: ' % len(commits))
2817 print('git log %s..%s' % (parent, ref_to_push))
2818 print('You can also use `git squash-branch` to squash these into a '
2819 'single commit.')
2820 ask_for_data('About to upload; enter to confirm.')
2821
2822 if options.reviewers or options.tbr_owners:
2823 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2824 change)
2825
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002826 # Extra options that can be specified at push time. Doc:
2827 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2828 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002829 if change_desc.get_reviewers(tbr_only=True):
2830 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2831 refspec_opts.append('l=Code-Review+1')
2832
Aaron Gable9b713dd2016-12-14 16:04:21 -08002833 if title:
2834 if not re.match(r'^[\w ]+$', title):
2835 title = re.sub(r'[^\w ]', '', title)
Aaron Gable856585d2017-01-23 15:32:00 -08002836 if not automatic_title:
2837 print('WARNING: Patchset title may only contain alphanumeric chars '
2838 'and spaces. Cleaned up title:\n%s' % title)
2839 if not options.force:
2840 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002841 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2842 # reverse on its side.
Aaron Gable9b713dd2016-12-14 16:04:21 -08002843 refspec_opts.append('m=' + title.replace(' ', '_'))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002844
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002845 if options.send_mail:
2846 if not change_desc.get_reviewers():
2847 DieWithError('Must specify reviewers to send email.')
2848 refspec_opts.append('notify=ALL')
2849 else:
2850 refspec_opts.append('notify=NONE')
2851
tandrii99a72f22016-08-17 14:33:24 -07002852 reviewers = change_desc.get_reviewers()
2853 if reviewers:
2854 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002855
agablec6787972016-09-09 16:13:34 -07002856 if options.private:
2857 refspec_opts.append('draft')
2858
rmistry9eadede2016-09-19 11:22:43 -07002859 if options.topic:
2860 # Documentation on Gerrit topics is here:
2861 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2862 refspec_opts.append('topic=%s' % options.topic)
2863
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002864 refspec_suffix = ''
2865 if refspec_opts:
2866 refspec_suffix = '%' + ','.join(refspec_opts)
2867 assert ' ' not in refspec_suffix, (
2868 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002869 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002870
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002871 try:
2872 push_stdout = gclient_utils.CheckCallAndFilter(
2873 ['git', 'push', gerrit_remote, refspec],
2874 print_stdout=True,
2875 # Flush after every line: useful for seeing progress when running as
2876 # recipe.
2877 filter_fn=lambda _: sys.stdout.flush())
2878 except subprocess2.CalledProcessError:
2879 DieWithError('Failed to create a change. Please examine output above '
2880 'for the reason of the failure. ')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002881
2882 if options.squash:
2883 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2884 change_numbers = [m.group(1)
2885 for m in map(regex.match, push_stdout.splitlines())
2886 if m]
2887 if len(change_numbers) != 1:
2888 DieWithError(
2889 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2890 'Change-Id: %s') % (len(change_numbers), change_id))
2891 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002892 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002893
2894 # Add cc's from the CC_LIST and --cc flag (if any).
2895 cc = self.GetCCList().split(',')
2896 if options.cc:
2897 cc.extend(options.cc)
2898 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002899 if change_desc.get_cced():
2900 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002901 if cc:
Aaron Gable5db82092016-11-17 10:52:08 -08002902 gerrit_util.AddReviewers(
Aaron Gable59f48512017-01-12 10:54:46 -08002903 self._GetGerritHost(), self.GetIssue(), cc,
2904 is_reviewer=False, notify=bool(options.send_mail))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002905 return 0
2906
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002907 def _AddChangeIdToCommitMessage(self, options, args):
2908 """Re-commits using the current message, assumes the commit hook is in
2909 place.
2910 """
2911 log_desc = options.message or CreateDescriptionFromLog(args)
2912 git_command = ['commit', '--amend', '-m', log_desc]
2913 RunGit(git_command)
2914 new_log_desc = CreateDescriptionFromLog(args)
2915 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002916 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002917 return new_log_desc
2918 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002919 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002920
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002921 def SetCQState(self, new_state):
2922 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002923 vote_map = {
2924 _CQState.NONE: 0,
2925 _CQState.DRY_RUN: 1,
2926 _CQState.COMMIT : 2,
2927 }
Andrii Shyshkalov828701b2016-12-09 10:46:47 +01002928 kwargs = {'labels': {'Commit-Queue': vote_map[new_state]}}
2929 if new_state == _CQState.DRY_RUN:
2930 # Don't spam everybody reviewer/owner.
2931 kwargs['notify'] = 'NONE'
2932 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(), **kwargs)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002933
tandriie113dfd2016-10-11 10:20:12 -07002934 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002935 try:
2936 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002937 except GerritChangeNotExists:
2938 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002939
2940 if data['status'] in ('ABANDONED', 'MERGED'):
2941 return 'CL %s is closed' % self.GetIssue()
2942
2943 def GetTryjobProperties(self, patchset=None):
2944 """Returns dictionary of properties to launch tryjob."""
2945 data = self._GetChangeDetail(['ALL_REVISIONS'])
2946 patchset = int(patchset or self.GetPatchset())
2947 assert patchset
2948 revision_data = None # Pylint wants it to be defined.
2949 for revision_data in data['revisions'].itervalues():
2950 if int(revision_data['_number']) == patchset:
2951 break
2952 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002953 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002954 (patchset, self.GetIssue()))
2955 return {
2956 'patch_issue': self.GetIssue(),
2957 'patch_set': patchset or self.GetPatchset(),
2958 'patch_project': data['project'],
2959 'patch_storage': 'gerrit',
2960 'patch_ref': revision_data['fetch']['http']['ref'],
2961 'patch_repository_url': revision_data['fetch']['http']['url'],
2962 'patch_gerrit_url': self.GetCodereviewServer(),
2963 }
tandriie113dfd2016-10-11 10:20:12 -07002964
tandriide281ae2016-10-12 06:02:30 -07002965 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002966 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002967
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002968
2969_CODEREVIEW_IMPLEMENTATIONS = {
2970 'rietveld': _RietveldChangelistImpl,
2971 'gerrit': _GerritChangelistImpl,
2972}
2973
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002974
iannuccie53c9352016-08-17 14:40:40 -07002975def _add_codereview_issue_select_options(parser, extra=""):
2976 _add_codereview_select_options(parser)
2977
2978 text = ('Operate on this issue number instead of the current branch\'s '
2979 'implicit issue.')
2980 if extra:
2981 text += ' '+extra
2982 parser.add_option('-i', '--issue', type=int, help=text)
2983
2984
2985def _process_codereview_issue_select_options(parser, options):
2986 _process_codereview_select_options(parser, options)
2987 if options.issue is not None and not options.forced_codereview:
2988 parser.error('--issue must be specified with either --rietveld or --gerrit')
2989
2990
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002991def _add_codereview_select_options(parser):
2992 """Appends --gerrit and --rietveld options to force specific codereview."""
2993 parser.codereview_group = optparse.OptionGroup(
2994 parser, 'EXPERIMENTAL! Codereview override options')
2995 parser.add_option_group(parser.codereview_group)
2996 parser.codereview_group.add_option(
2997 '--gerrit', action='store_true',
2998 help='Force the use of Gerrit for codereview')
2999 parser.codereview_group.add_option(
3000 '--rietveld', action='store_true',
3001 help='Force the use of Rietveld for codereview')
3002
3003
3004def _process_codereview_select_options(parser, options):
3005 if options.gerrit and options.rietveld:
3006 parser.error('Options --gerrit and --rietveld are mutually exclusive')
3007 options.forced_codereview = None
3008 if options.gerrit:
3009 options.forced_codereview = 'gerrit'
3010 elif options.rietveld:
3011 options.forced_codereview = 'rietveld'
3012
3013
tandriif9aefb72016-07-01 09:06:51 -07003014def _get_bug_line_values(default_project, bugs):
3015 """Given default_project and comma separated list of bugs, yields bug line
3016 values.
3017
3018 Each bug can be either:
3019 * a number, which is combined with default_project
3020 * string, which is left as is.
3021
3022 This function may produce more than one line, because bugdroid expects one
3023 project per line.
3024
3025 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3026 ['v8:123', 'chromium:789']
3027 """
3028 default_bugs = []
3029 others = []
3030 for bug in bugs.split(','):
3031 bug = bug.strip()
3032 if bug:
3033 try:
3034 default_bugs.append(int(bug))
3035 except ValueError:
3036 others.append(bug)
3037
3038 if default_bugs:
3039 default_bugs = ','.join(map(str, default_bugs))
3040 if default_project:
3041 yield '%s:%s' % (default_project, default_bugs)
3042 else:
3043 yield default_bugs
3044 for other in sorted(others):
3045 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3046 yield other
3047
3048
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003049class ChangeDescription(object):
3050 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003051 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003052 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00003053 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003054 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003055
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003056 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003057 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003058
agable@chromium.org42c20792013-09-12 17:34:49 +00003059 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003060 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003061 return '\n'.join(self._description_lines)
3062
3063 def set_description(self, desc):
3064 if isinstance(desc, basestring):
3065 lines = desc.splitlines()
3066 else:
3067 lines = [line.rstrip() for line in desc]
3068 while lines and not lines[0]:
3069 lines.pop(0)
3070 while lines and not lines[-1]:
3071 lines.pop(-1)
3072 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003073
piman@chromium.org336f9122014-09-04 02:16:55 +00003074 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003075 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003076 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00003077 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003078 return
agable@chromium.org42c20792013-09-12 17:34:49 +00003079 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003080
agable@chromium.org42c20792013-09-12 17:34:49 +00003081 # Get the set of R= and TBR= lines and remove them from the desciption.
3082 regexp = re.compile(self.R_LINE)
3083 matches = [regexp.match(line) for line in self._description_lines]
3084 new_desc = [l for i, l in enumerate(self._description_lines)
3085 if not matches[i]]
3086 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003087
agable@chromium.org42c20792013-09-12 17:34:49 +00003088 # Construct new unified R= and TBR= lines.
3089 r_names = []
3090 tbr_names = []
3091 for match in matches:
3092 if not match:
3093 continue
3094 people = cleanup_list([match.group(2).strip()])
3095 if match.group(1) == 'TBR':
3096 tbr_names.extend(people)
3097 else:
3098 r_names.extend(people)
3099 for name in r_names:
3100 if name not in reviewers:
3101 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003102 if add_owners_tbr:
3103 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003104 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003105 all_reviewers = set(tbr_names + reviewers)
3106 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3107 all_reviewers)
3108 tbr_names.extend(owners_db.reviewers_for(missing_files,
3109 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003110 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3111 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3112
3113 # Put the new lines in the description where the old first R= line was.
3114 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3115 if 0 <= line_loc < len(self._description_lines):
3116 if new_tbr_line:
3117 self._description_lines.insert(line_loc, new_tbr_line)
3118 if new_r_line:
3119 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003120 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003121 if new_r_line:
3122 self.append_footer(new_r_line)
3123 if new_tbr_line:
3124 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003125
tandriif9aefb72016-07-01 09:06:51 -07003126 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003127 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003128 self.set_description([
3129 '# Enter a description of the change.',
3130 '# This will be displayed on the codereview site.',
3131 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003132 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003133 '--------------------',
3134 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003135
agable@chromium.org42c20792013-09-12 17:34:49 +00003136 regexp = re.compile(self.BUG_LINE)
3137 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003138 prefix = settings.GetBugPrefix()
3139 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
3140 for value in values:
3141 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
3142 self.append_footer('BUG=%s' % value)
3143
agable@chromium.org42c20792013-09-12 17:34:49 +00003144 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003145 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003146 if not content:
3147 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003148 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003149
3150 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003151 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3152 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003153 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003154 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003155
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003156 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003157 """Adds a footer line to the description.
3158
3159 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3160 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3161 that Gerrit footers are always at the end.
3162 """
3163 parsed_footer_line = git_footers.parse_footer(line)
3164 if parsed_footer_line:
3165 # Line is a gerrit footer in the form: Footer-Key: any value.
3166 # Thus, must be appended observing Gerrit footer rules.
3167 self.set_description(
3168 git_footers.add_footer(self.description,
3169 key=parsed_footer_line[0],
3170 value=parsed_footer_line[1]))
3171 return
3172
3173 if not self._description_lines:
3174 self._description_lines.append(line)
3175 return
3176
3177 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3178 if gerrit_footers:
3179 # git_footers.split_footers ensures that there is an empty line before
3180 # actual (gerrit) footers, if any. We have to keep it that way.
3181 assert top_lines and top_lines[-1] == ''
3182 top_lines, separator = top_lines[:-1], top_lines[-1:]
3183 else:
3184 separator = [] # No need for separator if there are no gerrit_footers.
3185
3186 prev_line = top_lines[-1] if top_lines else ''
3187 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3188 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3189 top_lines.append('')
3190 top_lines.append(line)
3191 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003192
tandrii99a72f22016-08-17 14:33:24 -07003193 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003194 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003195 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003196 reviewers = [match.group(2).strip()
3197 for match in matches
3198 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003199 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003200
bradnelsond975b302016-10-23 12:20:23 -07003201 def get_cced(self):
3202 """Retrieves the list of reviewers."""
3203 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3204 cced = [match.group(2).strip() for match in matches if match]
3205 return cleanup_list(cced)
3206
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003207 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3208 """Updates this commit description given the parent.
3209
3210 This is essentially what Gnumbd used to do.
3211 Consult https://goo.gl/WMmpDe for more details.
3212 """
3213 assert parent_msg # No, orphan branch creation isn't supported.
3214 assert parent_hash
3215 assert dest_ref
3216 parent_footer_map = git_footers.parse_footers(parent_msg)
3217 # This will also happily parse svn-position, which GnumbD is no longer
3218 # supporting. While we'd generate correct footers, the verifier plugin
3219 # installed in Gerrit will block such commit (ie git push below will fail).
3220 parent_position = git_footers.get_position(parent_footer_map)
3221
3222 # Cherry-picks may have last line obscuring their prior footers,
3223 # from git_footers perspective. This is also what Gnumbd did.
3224 cp_line = None
3225 if (self._description_lines and
3226 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3227 cp_line = self._description_lines.pop()
3228
3229 top_lines, _, parsed_footers = git_footers.split_footers(self.description)
3230
3231 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3232 # user interference with actual footers we'd insert below.
3233 for i, (k, v) in enumerate(parsed_footers):
3234 if k.startswith('Cr-'):
3235 parsed_footers[i] = (k.replace('Cr-', 'Cr-Original-'), v)
3236
3237 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003238 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003239 if parent_position[0] == dest_ref:
3240 # Same branch as parent.
3241 number = int(parent_position[1]) + 1
3242 else:
3243 number = 1 # New branch, and extra lineage.
3244 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3245 int(parent_position[1])))
3246
3247 parsed_footers.append(('Cr-Commit-Position',
3248 '%s@{#%d}' % (dest_ref, number)))
3249 parsed_footers.extend(('Cr-Branched-From', v) for v in lineage)
3250
3251 self._description_lines = top_lines
3252 if cp_line:
3253 self._description_lines.append(cp_line)
3254 if self._description_lines[-1] != '':
3255 self._description_lines.append('') # Ensure footer separator.
3256 self._description_lines.extend('%s: %s' % kv for kv in parsed_footers)
3257
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003258
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003259def get_approving_reviewers(props):
3260 """Retrieves the reviewers that approved a CL from the issue properties with
3261 messages.
3262
3263 Note that the list may contain reviewers that are not committer, thus are not
3264 considered by the CQ.
3265 """
3266 return sorted(
3267 set(
3268 message['sender']
3269 for message in props['messages']
3270 if message['approval'] and message['sender'] in props['reviewers']
3271 )
3272 )
3273
3274
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003275def FindCodereviewSettingsFile(filename='codereview.settings'):
3276 """Finds the given file starting in the cwd and going up.
3277
3278 Only looks up to the top of the repository unless an
3279 'inherit-review-settings-ok' file exists in the root of the repository.
3280 """
3281 inherit_ok_file = 'inherit-review-settings-ok'
3282 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003283 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003284 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3285 root = '/'
3286 while True:
3287 if filename in os.listdir(cwd):
3288 if os.path.isfile(os.path.join(cwd, filename)):
3289 return open(os.path.join(cwd, filename))
3290 if cwd == root:
3291 break
3292 cwd = os.path.dirname(cwd)
3293
3294
3295def LoadCodereviewSettingsFromFile(fileobj):
3296 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003297 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003298
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003299 def SetProperty(name, setting, unset_error_ok=False):
3300 fullname = 'rietveld.' + name
3301 if setting in keyvals:
3302 RunGit(['config', fullname, keyvals[setting]])
3303 else:
3304 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3305
tandrii48df5812016-10-17 03:55:37 -07003306 if not keyvals.get('GERRIT_HOST', False):
3307 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003308 # Only server setting is required. Other settings can be absent.
3309 # In that case, we ignore errors raised during option deletion attempt.
3310 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003311 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003312 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3313 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003314 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003315 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3316 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003317 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003318 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003319 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3320 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003321
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003322 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003323 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003324
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003325 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003326 RunGit(['config', 'gerrit.squash-uploads',
3327 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003328
tandrii@chromium.org28253532016-04-14 13:46:56 +00003329 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003330 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003331 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3332
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003333 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3334 #should be of the form
3335 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3336 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
3337 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3338 keyvals['ORIGIN_URL_CONFIG']])
3339
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003340
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003341def urlretrieve(source, destination):
3342 """urllib is broken for SSL connections via a proxy therefore we
3343 can't use urllib.urlretrieve()."""
3344 with open(destination, 'w') as f:
3345 f.write(urllib2.urlopen(source).read())
3346
3347
ukai@chromium.org712d6102013-11-27 00:52:58 +00003348def hasSheBang(fname):
3349 """Checks fname is a #! script."""
3350 with open(fname) as f:
3351 return f.read(2).startswith('#!')
3352
3353
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003354# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3355def DownloadHooks(*args, **kwargs):
3356 pass
3357
3358
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003359def DownloadGerritHook(force):
3360 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003361
3362 Args:
3363 force: True to update hooks. False to install hooks if not present.
3364 """
3365 if not settings.GetIsGerrit():
3366 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003367 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003368 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3369 if not os.access(dst, os.X_OK):
3370 if os.path.exists(dst):
3371 if not force:
3372 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003373 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003374 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003375 if not hasSheBang(dst):
3376 DieWithError('Not a script: %s\n'
3377 'You need to download from\n%s\n'
3378 'into .git/hooks/commit-msg and '
3379 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003380 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3381 except Exception:
3382 if os.path.exists(dst):
3383 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003384 DieWithError('\nFailed to download hooks.\n'
3385 'You need to download from\n%s\n'
3386 'into .git/hooks/commit-msg and '
3387 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003388
3389
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003390
3391def GetRietveldCodereviewSettingsInteractively():
3392 """Prompt the user for settings."""
3393 server = settings.GetDefaultServerUrl(error_ok=True)
3394 prompt = 'Rietveld server (host[:port])'
3395 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3396 newserver = ask_for_data(prompt + ':')
3397 if not server and not newserver:
3398 newserver = DEFAULT_SERVER
3399 if newserver:
3400 newserver = gclient_utils.UpgradeToHttps(newserver)
3401 if newserver != server:
3402 RunGit(['config', 'rietveld.server', newserver])
3403
3404 def SetProperty(initial, caption, name, is_url):
3405 prompt = caption
3406 if initial:
3407 prompt += ' ("x" to clear) [%s]' % initial
3408 new_val = ask_for_data(prompt + ':')
3409 if new_val == 'x':
3410 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3411 elif new_val:
3412 if is_url:
3413 new_val = gclient_utils.UpgradeToHttps(new_val)
3414 if new_val != initial:
3415 RunGit(['config', 'rietveld.' + name, new_val])
3416
3417 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3418 SetProperty(settings.GetDefaultPrivateFlag(),
3419 'Private flag (rietveld only)', 'private', False)
3420 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3421 'tree-status-url', False)
3422 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3423 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3424 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3425 'run-post-upload-hook', False)
3426
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003427@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003428def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003429 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003430
tandrii5d0a0422016-09-14 06:24:35 -07003431 print('WARNING: git cl config works for Rietveld only')
3432 # TODO(tandrii): remove this once we switch to Gerrit.
3433 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003434 parser.add_option('--activate-update', action='store_true',
3435 help='activate auto-updating [rietveld] section in '
3436 '.git/config')
3437 parser.add_option('--deactivate-update', action='store_true',
3438 help='deactivate auto-updating [rietveld] section in '
3439 '.git/config')
3440 options, args = parser.parse_args(args)
3441
3442 if options.deactivate_update:
3443 RunGit(['config', 'rietveld.autoupdate', 'false'])
3444 return
3445
3446 if options.activate_update:
3447 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3448 return
3449
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003450 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003451 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003452 return 0
3453
3454 url = args[0]
3455 if not url.endswith('codereview.settings'):
3456 url = os.path.join(url, 'codereview.settings')
3457
3458 # Load code review settings and download hooks (if available).
3459 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3460 return 0
3461
3462
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003463def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003464 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003465 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3466 branch = ShortBranchName(branchref)
3467 _, args = parser.parse_args(args)
3468 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003469 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003470 return RunGit(['config', 'branch.%s.base-url' % branch],
3471 error_ok=False).strip()
3472 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003473 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003474 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3475 error_ok=False).strip()
3476
3477
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003478def color_for_status(status):
3479 """Maps a Changelist status to color, for CMDstatus and other tools."""
3480 return {
3481 'unsent': Fore.RED,
3482 'waiting': Fore.BLUE,
3483 'reply': Fore.YELLOW,
3484 'lgtm': Fore.GREEN,
3485 'commit': Fore.MAGENTA,
3486 'closed': Fore.CYAN,
3487 'error': Fore.WHITE,
3488 }.get(status, Fore.WHITE)
3489
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003490
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003491def get_cl_statuses(changes, fine_grained, max_processes=None):
3492 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003493
3494 If fine_grained is true, this will fetch CL statuses from the server.
3495 Otherwise, simply indicate if there's a matching url for the given branches.
3496
3497 If max_processes is specified, it is used as the maximum number of processes
3498 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3499 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003500
3501 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003502 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003503 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003504 upload.verbosity = 0
3505
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003506 if not changes:
3507 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003508
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003509 if not fine_grained:
3510 # Fast path which doesn't involve querying codereview servers.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003511 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003512 for cl in changes:
3513 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003514 return
3515
3516 # First, sort out authentication issues.
3517 logging.debug('ensuring credentials exist')
3518 for cl in changes:
3519 cl.EnsureAuthenticated(force=False, refresh=True)
3520
3521 def fetch(cl):
3522 try:
3523 return (cl, cl.GetStatus())
3524 except:
3525 # See http://crbug.com/629863.
3526 logging.exception('failed to fetch status for %s:', cl)
3527 raise
3528
3529 threads_count = len(changes)
3530 if max_processes:
3531 threads_count = max(1, min(threads_count, max_processes))
3532 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3533
3534 pool = ThreadPool(threads_count)
3535 fetched_cls = set()
3536 try:
3537 it = pool.imap_unordered(fetch, changes).__iter__()
3538 while True:
3539 try:
3540 cl, status = it.next(timeout=5)
3541 except multiprocessing.TimeoutError:
3542 break
3543 fetched_cls.add(cl)
3544 yield cl, status
3545 finally:
3546 pool.close()
3547
3548 # Add any branches that failed to fetch.
3549 for cl in set(changes) - fetched_cls:
3550 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003551
rmistry@google.com2dd99862015-06-22 12:22:18 +00003552
3553def upload_branch_deps(cl, args):
3554 """Uploads CLs of local branches that are dependents of the current branch.
3555
3556 If the local branch dependency tree looks like:
3557 test1 -> test2.1 -> test3.1
3558 -> test3.2
3559 -> test2.2 -> test3.3
3560
3561 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3562 run on the dependent branches in this order:
3563 test2.1, test3.1, test3.2, test2.2, test3.3
3564
3565 Note: This function does not rebase your local dependent branches. Use it when
3566 you make a change to the parent branch that will not conflict with its
3567 dependent branches, and you would like their dependencies updated in
3568 Rietveld.
3569 """
3570 if git_common.is_dirty_git_tree('upload-branch-deps'):
3571 return 1
3572
3573 root_branch = cl.GetBranch()
3574 if root_branch is None:
3575 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3576 'Get on a branch!')
3577 if not cl.GetIssue() or not cl.GetPatchset():
3578 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3579 'patchset dependencies without an uploaded CL.')
3580
3581 branches = RunGit(['for-each-ref',
3582 '--format=%(refname:short) %(upstream:short)',
3583 'refs/heads'])
3584 if not branches:
3585 print('No local branches found.')
3586 return 0
3587
3588 # Create a dictionary of all local branches to the branches that are dependent
3589 # on it.
3590 tracked_to_dependents = collections.defaultdict(list)
3591 for b in branches.splitlines():
3592 tokens = b.split()
3593 if len(tokens) == 2:
3594 branch_name, tracked = tokens
3595 tracked_to_dependents[tracked].append(branch_name)
3596
vapiera7fbd5a2016-06-16 09:17:49 -07003597 print()
3598 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003599 dependents = []
3600 def traverse_dependents_preorder(branch, padding=''):
3601 dependents_to_process = tracked_to_dependents.get(branch, [])
3602 padding += ' '
3603 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003604 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003605 dependents.append(dependent)
3606 traverse_dependents_preorder(dependent, padding)
3607 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003608 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003609
3610 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003611 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003612 return 0
3613
vapiera7fbd5a2016-06-16 09:17:49 -07003614 print('This command will checkout all dependent branches and run '
3615 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003616 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3617
andybons@chromium.org962f9462016-02-03 20:00:42 +00003618 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003619 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003620 args.extend(['-t', 'Updated patchset dependency'])
3621
rmistry@google.com2dd99862015-06-22 12:22:18 +00003622 # Record all dependents that failed to upload.
3623 failures = {}
3624 # Go through all dependents, checkout the branch and upload.
3625 try:
3626 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003627 print()
3628 print('--------------------------------------')
3629 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003630 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003631 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003632 try:
3633 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003634 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003635 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003636 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003637 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003638 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003639 finally:
3640 # Swap back to the original root branch.
3641 RunGit(['checkout', '-q', root_branch])
3642
vapiera7fbd5a2016-06-16 09:17:49 -07003643 print()
3644 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003645 for dependent_branch in dependents:
3646 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003647 print(' %s : %s' % (dependent_branch, upload_status))
3648 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003649
3650 return 0
3651
3652
kmarshall3bff56b2016-06-06 18:31:47 -07003653def CMDarchive(parser, args):
3654 """Archives and deletes branches associated with closed changelists."""
3655 parser.add_option(
3656 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003657 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003658 parser.add_option(
3659 '-f', '--force', action='store_true',
3660 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003661 parser.add_option(
3662 '-d', '--dry-run', action='store_true',
3663 help='Skip the branch tagging and removal steps.')
3664 parser.add_option(
3665 '-t', '--notags', action='store_true',
3666 help='Do not tag archived branches. '
3667 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003668
3669 auth.add_auth_options(parser)
3670 options, args = parser.parse_args(args)
3671 if args:
3672 parser.error('Unsupported args: %s' % ' '.join(args))
3673 auth_config = auth.extract_auth_config_from_options(options)
3674
3675 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3676 if not branches:
3677 return 0
3678
vapiera7fbd5a2016-06-16 09:17:49 -07003679 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003680 changes = [Changelist(branchref=b, auth_config=auth_config)
3681 for b in branches.splitlines()]
3682 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3683 statuses = get_cl_statuses(changes,
3684 fine_grained=True,
3685 max_processes=options.maxjobs)
3686 proposal = [(cl.GetBranch(),
3687 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3688 for cl, status in statuses
3689 if status == 'closed']
3690 proposal.sort()
3691
3692 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003693 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003694 return 0
3695
3696 current_branch = GetCurrentBranch()
3697
vapiera7fbd5a2016-06-16 09:17:49 -07003698 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003699 if options.notags:
3700 for next_item in proposal:
3701 print(' ' + next_item[0])
3702 else:
3703 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3704 for next_item in proposal:
3705 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003706
kmarshall9249e012016-08-23 12:02:16 -07003707 # Quit now on precondition failure or if instructed by the user, either
3708 # via an interactive prompt or by command line flags.
3709 if options.dry_run:
3710 print('\nNo changes were made (dry run).\n')
3711 return 0
3712 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003713 print('You are currently on a branch \'%s\' which is associated with a '
3714 'closed codereview issue, so archive cannot proceed. Please '
3715 'checkout another branch and run this command again.' %
3716 current_branch)
3717 return 1
kmarshall9249e012016-08-23 12:02:16 -07003718 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003719 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3720 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003721 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003722 return 1
3723
3724 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003725 if not options.notags:
3726 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003727 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003728
vapiera7fbd5a2016-06-16 09:17:49 -07003729 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003730
3731 return 0
3732
3733
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003734def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003735 """Show status of changelists.
3736
3737 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003738 - Red not sent for review or broken
3739 - Blue waiting for review
3740 - Yellow waiting for you to reply to review
3741 - Green LGTM'ed
3742 - Magenta in the commit queue
3743 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003744
3745 Also see 'git cl comments'.
3746 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003747 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003748 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003749 parser.add_option('-f', '--fast', action='store_true',
3750 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003751 parser.add_option(
3752 '-j', '--maxjobs', action='store', type=int,
3753 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003754
3755 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003756 _add_codereview_issue_select_options(
3757 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003758 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003759 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003760 if args:
3761 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003762 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003763
iannuccie53c9352016-08-17 14:40:40 -07003764 if options.issue is not None and not options.field:
3765 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003766
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003767 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003768 cl = Changelist(auth_config=auth_config, issue=options.issue,
3769 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003770 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003771 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003772 elif options.field == 'id':
3773 issueid = cl.GetIssue()
3774 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003775 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003776 elif options.field == 'patch':
3777 patchset = cl.GetPatchset()
3778 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003779 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003780 elif options.field == 'status':
3781 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003782 elif options.field == 'url':
3783 url = cl.GetIssueURL()
3784 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003785 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003786 return 0
3787
3788 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3789 if not branches:
3790 print('No local branch found.')
3791 return 0
3792
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003793 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003794 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003795 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003796 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003797 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003798 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003799 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003800
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003801 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003802 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3803 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3804 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003805 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003806 c, status = output.next()
3807 branch_statuses[c.GetBranch()] = status
3808 status = branch_statuses.pop(branch)
3809 url = cl.GetIssueURL()
3810 if url and (not status or status == 'error'):
3811 # The issue probably doesn't exist anymore.
3812 url += ' (broken)'
3813
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003814 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003815 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003816 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003817 color = ''
3818 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003819 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003820 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003821 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003822 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003823
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003824
3825 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07003826 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003827 print('Current branch: %s' % branch)
3828 for cl in changes:
3829 if cl.GetBranch() == branch:
3830 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003831 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003832 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003833 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003834 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003835 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003836 print('Issue description:')
3837 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003838 return 0
3839
3840
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003841def colorize_CMDstatus_doc():
3842 """To be called once in main() to add colors to git cl status help."""
3843 colors = [i for i in dir(Fore) if i[0].isupper()]
3844
3845 def colorize_line(line):
3846 for color in colors:
3847 if color in line.upper():
3848 # Extract whitespaces first and the leading '-'.
3849 indent = len(line) - len(line.lstrip(' ')) + 1
3850 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3851 return line
3852
3853 lines = CMDstatus.__doc__.splitlines()
3854 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3855
3856
phajdan.jre328cf92016-08-22 04:12:17 -07003857def write_json(path, contents):
3858 with open(path, 'w') as f:
3859 json.dump(contents, f)
3860
3861
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003862@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003863def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003864 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003865
3866 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003867 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003868 parser.add_option('-r', '--reverse', action='store_true',
3869 help='Lookup the branch(es) for the specified issues. If '
3870 'no issues are specified, all branches with mapped '
3871 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003872 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003873 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003874 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003875 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003876
dnj@chromium.org406c4402015-03-03 17:22:28 +00003877 if options.reverse:
3878 branches = RunGit(['for-each-ref', 'refs/heads',
3879 '--format=%(refname:short)']).splitlines()
3880
3881 # Reverse issue lookup.
3882 issue_branch_map = {}
3883 for branch in branches:
3884 cl = Changelist(branchref=branch)
3885 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3886 if not args:
3887 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003888 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003889 for issue in args:
3890 if not issue:
3891 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003892 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003893 print('Branch for issue number %s: %s' % (
3894 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003895 if options.json:
3896 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003897 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003898 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003899 if len(args) > 0:
3900 try:
3901 issue = int(args[0])
3902 except ValueError:
3903 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003904 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003905 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003906 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003907 if options.json:
3908 write_json(options.json, {
3909 'issue': cl.GetIssue(),
3910 'issue_url': cl.GetIssueURL(),
3911 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003912 return 0
3913
3914
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003915def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003916 """Shows or posts review comments for any changelist."""
3917 parser.add_option('-a', '--add-comment', dest='comment',
3918 help='comment to add to an issue')
3919 parser.add_option('-i', dest='issue',
3920 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003921 parser.add_option('-j', '--json-file',
3922 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003923 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003924 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003925 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003926
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003927 issue = None
3928 if options.issue:
3929 try:
3930 issue = int(options.issue)
3931 except ValueError:
3932 DieWithError('A review issue id is expected to be a number')
3933
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003934 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003935
3936 if options.comment:
3937 cl.AddComment(options.comment)
3938 return 0
3939
3940 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003941 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003942 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003943 summary.append({
3944 'date': message['date'],
3945 'lgtm': False,
3946 'message': message['text'],
3947 'not_lgtm': False,
3948 'sender': message['sender'],
3949 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003950 if message['disapproval']:
3951 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003952 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003953 elif message['approval']:
3954 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003955 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003956 elif message['sender'] == data['owner_email']:
3957 color = Fore.MAGENTA
3958 else:
3959 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003960 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003961 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003962 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003963 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003964 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003965 if options.json_file:
3966 with open(options.json_file, 'wb') as f:
3967 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003968 return 0
3969
3970
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003971@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003972def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003973 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003974 parser.add_option('-d', '--display', action='store_true',
3975 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003976 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003977 help='New description to set for this issue (- for stdin, '
3978 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003979 parser.add_option('-f', '--force', action='store_true',
3980 help='Delete any unpublished Gerrit edits for this issue '
3981 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003982
3983 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003984 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003985 options, args = parser.parse_args(args)
3986 _process_codereview_select_options(parser, options)
3987
3988 target_issue = None
3989 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003990 target_issue = ParseIssueNumberArgument(args[0])
3991 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003992 parser.print_help()
3993 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003994
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003995 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003996
martiniss6eda05f2016-06-30 10:18:35 -07003997 kwargs = {
3998 'auth_config': auth_config,
3999 'codereview': options.forced_codereview,
4000 }
4001 if target_issue:
4002 kwargs['issue'] = target_issue.issue
4003 if options.forced_codereview == 'rietveld':
4004 kwargs['rietveld_server'] = target_issue.hostname
4005
4006 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004007
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004008 if not cl.GetIssue():
4009 DieWithError('This branch has no associated changelist.')
4010 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004011
smut@google.com34fb6b12015-07-13 20:03:26 +00004012 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004013 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004014 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004015
4016 if options.new_description:
4017 text = options.new_description
4018 if text == '-':
4019 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004020 elif text == '+':
4021 base_branch = cl.GetCommonAncestorWithUpstream()
4022 change = cl.GetChange(base_branch, None, local_description=True)
4023 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004024
4025 description.set_description(text)
4026 else:
4027 description.prompt()
4028
wychen@chromium.org063e4e52015-04-03 06:51:44 +00004029 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004030 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004031 return 0
4032
4033
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004034def CreateDescriptionFromLog(args):
4035 """Pulls out the commit log to use as a base for the CL description."""
4036 log_args = []
4037 if len(args) == 1 and not args[0].endswith('.'):
4038 log_args = [args[0] + '..']
4039 elif len(args) == 1 and args[0].endswith('...'):
4040 log_args = [args[0][:-1]]
4041 elif len(args) == 2:
4042 log_args = [args[0] + '..' + args[1]]
4043 else:
4044 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004045 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004046
4047
thestig@chromium.org44202a22014-03-11 19:22:18 +00004048def CMDlint(parser, args):
4049 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004050 parser.add_option('--filter', action='append', metavar='-x,+y',
4051 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004052 auth.add_auth_options(parser)
4053 options, args = parser.parse_args(args)
4054 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004055
4056 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004057 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004058 try:
4059 import cpplint
4060 import cpplint_chromium
4061 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004062 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004063 return 1
4064
4065 # Change the current working directory before calling lint so that it
4066 # shows the correct base.
4067 previous_cwd = os.getcwd()
4068 os.chdir(settings.GetRoot())
4069 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004070 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004071 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4072 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004073 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004074 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004075 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004076
4077 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004078 command = args + files
4079 if options.filter:
4080 command = ['--filter=' + ','.join(options.filter)] + command
4081 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004082
4083 white_regex = re.compile(settings.GetLintRegex())
4084 black_regex = re.compile(settings.GetLintIgnoreRegex())
4085 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4086 for filename in filenames:
4087 if white_regex.match(filename):
4088 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004089 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004090 else:
4091 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4092 extra_check_functions)
4093 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004094 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004095 finally:
4096 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004097 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004098 if cpplint._cpplint_state.error_count != 0:
4099 return 1
4100 return 0
4101
4102
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004103def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004104 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004105 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004106 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004107 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004108 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004109 auth.add_auth_options(parser)
4110 options, args = parser.parse_args(args)
4111 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004112
sbc@chromium.org71437c02015-04-09 19:29:40 +00004113 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004114 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004115 return 1
4116
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004117 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004118 if args:
4119 base_branch = args[0]
4120 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004121 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004122 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004123
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004124 cl.RunHook(
4125 committing=not options.upload,
4126 may_prompt=False,
4127 verbose=options.verbose,
4128 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004129 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004130
4131
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004132def GenerateGerritChangeId(message):
4133 """Returns Ixxxxxx...xxx change id.
4134
4135 Works the same way as
4136 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4137 but can be called on demand on all platforms.
4138
4139 The basic idea is to generate git hash of a state of the tree, original commit
4140 message, author/committer info and timestamps.
4141 """
4142 lines = []
4143 tree_hash = RunGitSilent(['write-tree'])
4144 lines.append('tree %s' % tree_hash.strip())
4145 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4146 if code == 0:
4147 lines.append('parent %s' % parent.strip())
4148 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4149 lines.append('author %s' % author.strip())
4150 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4151 lines.append('committer %s' % committer.strip())
4152 lines.append('')
4153 # Note: Gerrit's commit-hook actually cleans message of some lines and
4154 # whitespace. This code is not doing this, but it clearly won't decrease
4155 # entropy.
4156 lines.append(message)
4157 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4158 stdin='\n'.join(lines))
4159 return 'I%s' % change_hash.strip()
4160
4161
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004162def GetTargetRef(remote, remote_branch, target_branch, pending_prefix_check,
4163 remote_url=None):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004164 """Computes the remote branch ref to use for the CL.
4165
4166 Args:
4167 remote (str): The git remote for the CL.
4168 remote_branch (str): The git remote branch for the CL.
4169 target_branch (str): The target branch specified by the user.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004170 pending_prefix_check (bool): If true, determines if pending_prefix should be
4171 used.
4172 remote_url (str): Only used for checking if pending_prefix should be used.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004173 """
4174 if not (remote and remote_branch):
4175 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004176
wittman@chromium.org455dc922015-01-26 20:15:50 +00004177 if target_branch:
4178 # Cannonicalize branch references to the equivalent local full symbolic
4179 # refs, which are then translated into the remote full symbolic refs
4180 # below.
4181 if '/' not in target_branch:
4182 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4183 else:
4184 prefix_replacements = (
4185 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4186 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4187 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4188 )
4189 match = None
4190 for regex, replacement in prefix_replacements:
4191 match = re.search(regex, target_branch)
4192 if match:
4193 remote_branch = target_branch.replace(match.group(0), replacement)
4194 break
4195 if not match:
4196 # This is a branch path but not one we recognize; use as-is.
4197 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004198 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4199 # Handle the refs that need to land in different refs.
4200 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004201
wittman@chromium.org455dc922015-01-26 20:15:50 +00004202 # Create the true path to the remote branch.
4203 # Does the following translation:
4204 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4205 # * refs/remotes/origin/master -> refs/heads/master
4206 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4207 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4208 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4209 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4210 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4211 'refs/heads/')
4212 elif remote_branch.startswith('refs/remotes/branch-heads'):
4213 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004214
4215 if pending_prefix_check:
4216 # If a pending prefix exists then replace refs/ with it.
4217 state = _GitNumbererState.load(remote_url, remote_branch)
4218 if state.pending_prefix:
4219 remote_branch = remote_branch.replace('refs/', state.pending_prefix)
wittman@chromium.org455dc922015-01-26 20:15:50 +00004220 return remote_branch
4221
4222
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004223def cleanup_list(l):
4224 """Fixes a list so that comma separated items are put as individual items.
4225
4226 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4227 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4228 """
4229 items = sum((i.split(',') for i in l), [])
4230 stripped_items = (i.strip() for i in items)
4231 return sorted(filter(None, stripped_items))
4232
4233
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004234@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004235def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004236 """Uploads the current changelist to codereview.
4237
4238 Can skip dependency patchset uploads for a branch by running:
4239 git config branch.branch_name.skip-deps-uploads True
4240 To unset run:
4241 git config --unset branch.branch_name.skip-deps-uploads
4242 Can also set the above globally by using the --global flag.
4243 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004244 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4245 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004246 parser.add_option('--bypass-watchlists', action='store_true',
4247 dest='bypass_watchlists',
4248 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004249 parser.add_option('-f', action='store_true', dest='force',
4250 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004251 parser.add_option('--message', '-m', dest='message',
4252 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004253 parser.add_option('-b', '--bug',
4254 help='pre-populate the bug number(s) for this issue. '
4255 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004256 parser.add_option('--message-file', dest='message_file',
4257 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004258 parser.add_option('--title', '-t', dest='title',
4259 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004260 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004261 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004262 help='reviewer email addresses')
4263 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004264 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004265 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004266 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004267 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004268 parser.add_option('--emulate_svn_auto_props',
4269 '--emulate-svn-auto-props',
4270 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004271 dest="emulate_svn_auto_props",
4272 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004273 parser.add_option('-c', '--use-commit-queue', action='store_true',
4274 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004275 parser.add_option('--private', action='store_true',
4276 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004277 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004278 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004279 metavar='TARGET',
4280 help='Apply CL to remote ref TARGET. ' +
4281 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004282 parser.add_option('--squash', action='store_true',
4283 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004284 parser.add_option('--no-squash', action='store_true',
4285 help='Don\'t squash multiple commits into one ' +
4286 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004287 parser.add_option('--topic', default=None,
4288 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004289 parser.add_option('--email', default=None,
4290 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004291 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4292 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004293 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4294 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004295 help='Send the patchset to do a CQ dry run right after '
4296 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004297 parser.add_option('--dependencies', action='store_true',
4298 help='Uploads CLs of all the local branches that depend on '
4299 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004300
rmistry@google.com2dd99862015-06-22 12:22:18 +00004301 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004302 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004303 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004304 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004305 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004306 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004307 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004308
sbc@chromium.org71437c02015-04-09 19:29:40 +00004309 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004310 return 1
4311
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004312 options.reviewers = cleanup_list(options.reviewers)
4313 options.cc = cleanup_list(options.cc)
4314
tandriib80458a2016-06-23 12:20:07 -07004315 if options.message_file:
4316 if options.message:
4317 parser.error('only one of --message and --message-file allowed.')
4318 options.message = gclient_utils.FileRead(options.message_file)
4319 options.message_file = None
4320
tandrii4d0545a2016-07-06 03:56:49 -07004321 if options.cq_dry_run and options.use_commit_queue:
4322 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4323
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004324 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4325 settings.GetIsGerrit()
4326
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004327 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004328 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004329
4330
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004331def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004332 print()
4333 print('Waiting for commit to be landed on %s...' % real_ref)
4334 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004335 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4336 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004337 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004338
4339 loop = 0
4340 while True:
4341 sys.stdout.write('fetching (%d)... \r' % loop)
4342 sys.stdout.flush()
4343 loop += 1
4344
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004345 if mirror:
4346 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004347 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4348 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4349 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4350 for commit in commits.splitlines():
4351 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004352 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004353 return commit
4354
4355 current_rev = to_rev
4356
4357
tandriibf429402016-09-14 07:09:12 -07004358def PushToGitPending(remote, pending_ref):
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004359 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4360
4361 Returns:
4362 (retcode of last operation, output log of last operation).
4363 """
4364 assert pending_ref.startswith('refs/'), pending_ref
4365 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4366 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4367 code = 0
4368 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004369 max_attempts = 3
4370 attempts_left = max_attempts
4371 while attempts_left:
4372 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004373 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004374 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004375
4376 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004377 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004378 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004379 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004380 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004381 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004382 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004383 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004384 continue
4385
4386 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004387 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004388 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004389 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004390 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004391 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4392 'the following files have merge conflicts:' % pending_ref)
4393 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4394 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004395 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004396 return code, out
4397
4398 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004399 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004400 code, out = RunGitWithCode(
4401 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4402 if code == 0:
4403 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004404 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004405 return code, out
4406
vapiera7fbd5a2016-06-16 09:17:49 -07004407 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004408 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004409 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004410 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004411 print('Fatal push error. Make sure your .netrc credentials and git '
4412 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004413 return code, out
4414
vapiera7fbd5a2016-06-16 09:17:49 -07004415 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004416 return code, out
4417
4418
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004419def IsFatalPushFailure(push_stdout):
4420 """True if retrying push won't help."""
4421 return '(prohibited by Gerrit)' in push_stdout
4422
4423
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004424@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004425def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004426 """DEPRECATED: Used to commit the current changelist via git-svn."""
4427 message = ('git-cl no longer supports committing to SVN repositories via '
4428 'git-svn. You probably want to use `git cl land` instead.')
4429 print(message)
4430 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004431
4432
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004433@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004434def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004435 """Commits the current changelist via git.
4436
4437 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4438 upstream and closes the issue automatically and atomically.
4439
4440 Otherwise (in case of Rietveld):
4441 Squashes branch into a single commit.
4442 Updates commit message with metadata (e.g. pointer to review).
4443 Pushes the code upstream.
4444 Updates review and closes.
4445 """
4446 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4447 help='bypass upload presubmit hook')
4448 parser.add_option('-m', dest='message',
4449 help="override review description")
4450 parser.add_option('-f', action='store_true', dest='force',
4451 help="force yes to questions (don't prompt)")
4452 parser.add_option('-c', dest='contributor',
4453 help="external contributor for patch (appended to " +
4454 "description and used as author for git). Should be " +
4455 "formatted as 'First Last <email@example.com>'")
4456 add_git_similarity(parser)
4457 auth.add_auth_options(parser)
4458 (options, args) = parser.parse_args(args)
4459 auth_config = auth.extract_auth_config_from_options(options)
4460
4461 cl = Changelist(auth_config=auth_config)
4462
4463 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4464 if cl.IsGerrit():
4465 if options.message:
4466 # This could be implemented, but it requires sending a new patch to
4467 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4468 # Besides, Gerrit has the ability to change the commit message on submit
4469 # automatically, thus there is no need to support this option (so far?).
4470 parser.error('-m MESSAGE option is not supported for Gerrit.')
4471 if options.contributor:
4472 parser.error(
4473 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4474 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4475 'the contributor\'s "name <email>". If you can\'t upload such a '
4476 'commit for review, contact your repository admin and request'
4477 '"Forge-Author" permission.')
4478 if not cl.GetIssue():
4479 DieWithError('You must upload the change first to Gerrit.\n'
4480 ' If you would rather have `git cl land` upload '
4481 'automatically for you, see http://crbug.com/642759')
4482 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4483 options.verbose)
4484
4485 current = cl.GetBranch()
4486 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4487 if remote == '.':
4488 print()
4489 print('Attempting to push branch %r into another local branch!' % current)
4490 print()
4491 print('Either reparent this branch on top of origin/master:')
4492 print(' git reparent-branch --root')
4493 print()
4494 print('OR run `git rebase-update` if you think the parent branch is ')
4495 print('already committed.')
4496 print()
4497 print(' Current parent: %r' % upstream_branch)
4498 return 1
4499
4500 if not args:
4501 # Default to merging against our best guess of the upstream branch.
4502 args = [cl.GetUpstreamBranch()]
4503
4504 if options.contributor:
4505 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
4506 print("Please provide contibutor as 'First Last <email@example.com>'")
4507 return 1
4508
4509 base_branch = args[0]
4510
4511 if git_common.is_dirty_git_tree('land'):
4512 return 1
4513
4514 # This rev-list syntax means "show all commits not in my branch that
4515 # are in base_branch".
4516 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4517 base_branch]).splitlines()
4518 if upstream_commits:
4519 print('Base branch "%s" has %d commits '
4520 'not in this branch.' % (base_branch, len(upstream_commits)))
4521 print('Run "git merge %s" before attempting to land.' % base_branch)
4522 return 1
4523
4524 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
4525 if not options.bypass_hooks:
4526 author = None
4527 if options.contributor:
4528 author = re.search(r'\<(.*)\>', options.contributor).group(1)
4529 hook_results = cl.RunHook(
4530 committing=True,
4531 may_prompt=not options.force,
4532 verbose=options.verbose,
4533 change=cl.GetChange(merge_base, author))
4534 if not hook_results.should_continue():
4535 return 1
4536
4537 # Check the tree status if the tree status URL is set.
4538 status = GetTreeStatus()
4539 if 'closed' == status:
4540 print('The tree is closed. Please wait for it to reopen. Use '
4541 '"git cl land --bypass-hooks" to commit on a closed tree.')
4542 return 1
4543 elif 'unknown' == status:
4544 print('Unable to determine tree status. Please verify manually and '
4545 'use "git cl land --bypass-hooks" to commit on a closed tree.')
4546 return 1
4547
4548 change_desc = ChangeDescription(options.message)
4549 if not change_desc.description and cl.GetIssue():
4550 change_desc = ChangeDescription(cl.GetDescription())
4551
4552 if not change_desc.description:
4553 if not cl.GetIssue() and options.bypass_hooks:
4554 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
4555 else:
4556 print('No description set.')
4557 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
4558 return 1
4559
4560 # Keep a separate copy for the commit message, because the commit message
4561 # contains the link to the Rietveld issue, while the Rietveld message contains
4562 # the commit viewvc url.
4563 if cl.GetIssue():
4564 change_desc.update_reviewers(cl.GetApprovingReviewers())
4565
4566 commit_desc = ChangeDescription(change_desc.description)
4567 if cl.GetIssue():
4568 # Xcode won't linkify this URL unless there is a non-whitespace character
4569 # after it. Add a period on a new line to circumvent this. Also add a space
4570 # before the period to make sure that Gitiles continues to correctly resolve
4571 # the URL.
4572 commit_desc.append_footer('Review-Url: %s .' % cl.GetIssueURL())
4573 if options.contributor:
4574 commit_desc.append_footer('Patch from %s.' % options.contributor)
4575
4576 print('Description:')
4577 print(commit_desc.description)
4578
4579 branches = [merge_base, cl.GetBranchRef()]
4580 if not options.force:
4581 print_stats(options.similarity, options.find_copies, branches)
4582
4583 # We want to squash all this branch's commits into one commit with the proper
4584 # description. We do this by doing a "reset --soft" to the base branch (which
4585 # keeps the working copy the same), then landing that.
4586 MERGE_BRANCH = 'git-cl-commit'
4587 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4588 # Delete the branches if they exist.
4589 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4590 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4591 result = RunGitWithCode(showref_cmd)
4592 if result[0] == 0:
4593 RunGit(['branch', '-D', branch])
4594
4595 # We might be in a directory that's present in this branch but not in the
4596 # trunk. Move up to the top of the tree so that git commands that expect a
4597 # valid CWD won't fail after we check out the merge branch.
4598 rel_base_path = settings.GetRelativeRoot()
4599 if rel_base_path:
4600 os.chdir(rel_base_path)
4601
4602 # Stuff our change into the merge branch.
4603 # We wrap in a try...finally block so if anything goes wrong,
4604 # we clean up the branches.
4605 retcode = -1
4606 pushed_to_pending = False
4607 pending_ref = None
4608 revision = None
4609 try:
4610 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
4611 RunGit(['reset', '--soft', merge_base])
4612 if options.contributor:
4613 RunGit(
4614 [
4615 'commit', '--author', options.contributor,
4616 '-m', commit_desc.description,
4617 ])
4618 else:
4619 RunGit(['commit', '-m', commit_desc.description])
4620
4621 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
4622 mirror = settings.GetGitMirror(remote)
4623 if mirror:
4624 pushurl = mirror.url
4625 git_numberer = _GitNumbererState.load(pushurl, branch)
4626 else:
4627 pushurl = remote # Usually, this is 'origin'.
4628 git_numberer = _GitNumbererState.load(
4629 RunGit(['config', 'remote.%s.url' % remote]).strip(), branch)
4630
4631 if git_numberer.should_add_git_number:
4632 # TODO(tandrii): run git fetch in a loop + autorebase when there there
4633 # is no pending ref to push to?
4634 logging.debug('Adding git number footers')
4635 parent_msg = RunGit(['show', '-s', '--format=%B', merge_base]).strip()
4636 commit_desc.update_with_git_number_footers(merge_base, parent_msg,
4637 branch)
4638 # Ensure timestamps are monotonically increasing.
4639 timestamp = max(1 + _get_committer_timestamp(merge_base),
4640 _get_committer_timestamp('HEAD'))
4641 _git_amend_head(commit_desc.description, timestamp)
4642 change_desc = ChangeDescription(commit_desc.description)
4643 # If gnumbd is sitll ON and we ultimately push to branch with
4644 # pending_prefix, gnumbd will modify footers we've just inserted with
4645 # 'Original-', which is annoying but still technically correct.
4646
4647 pending_prefix = git_numberer.pending_prefix
4648 if not pending_prefix or branch.startswith(pending_prefix):
4649 # If not using refs/pending/heads/* at all, or target ref is already set
4650 # to pending, then push to the target ref directly.
4651 # NB(tandrii): I think branch.startswith(pending_prefix) never happens
4652 # in practise. I really tried to create a new branch tracking
4653 # refs/pending/heads/master directly and git cl land failed long before
4654 # reaching this. Disagree? Comment on http://crbug.com/642493.
4655 if pending_prefix:
4656 print('\n\nYOU GOT A CHANCE TO WIN A FREE GIFT!\n\n'
4657 'Grab your .git/config, add instructions how to reproduce '
4658 'this, and post it to http://crbug.com/642493.\n'
4659 'The first reporter gets a free "Black Swan" book from '
4660 'tandrii@\n\n')
4661 retcode, output = RunGitWithCode(
4662 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
4663 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
4664 else:
4665 # Cherry-pick the change on top of pending ref and then push it.
4666 assert branch.startswith('refs/'), branch
4667 assert pending_prefix[-1] == '/', pending_prefix
4668 pending_ref = pending_prefix + branch[len('refs/'):]
4669 retcode, output = PushToGitPending(pushurl, pending_ref)
4670 pushed_to_pending = (retcode == 0)
4671
4672 if retcode == 0:
4673 revision = RunGit(['rev-parse', 'HEAD']).strip()
4674 logging.debug(output)
4675 except: # pylint: disable=bare-except
4676 if _IS_BEING_TESTED:
4677 logging.exception('this is likely your ACTUAL cause of test failure.\n'
4678 + '-' * 30 + '8<' + '-' * 30)
4679 logging.error('\n' + '-' * 30 + '8<' + '-' * 30 + '\n\n\n')
4680 raise
4681 finally:
4682 # And then swap back to the original branch and clean up.
4683 RunGit(['checkout', '-q', cl.GetBranch()])
4684 RunGit(['branch', '-D', MERGE_BRANCH])
4685
4686 if not revision:
4687 print('Failed to push. If this persists, please file a bug.')
4688 return 1
4689
4690 killed = False
4691 if pushed_to_pending:
4692 try:
4693 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4694 # We set pushed_to_pending to False, since it made it all the way to the
4695 # real ref.
4696 pushed_to_pending = False
4697 except KeyboardInterrupt:
4698 killed = True
4699
4700 if cl.GetIssue():
4701 to_pending = ' to pending queue' if pushed_to_pending else ''
4702 viewvc_url = settings.GetViewVCUrl()
4703 if not to_pending:
4704 if viewvc_url and revision:
4705 change_desc.append_footer(
4706 'Committed: %s%s' % (viewvc_url, revision))
4707 elif revision:
4708 change_desc.append_footer('Committed: %s' % (revision,))
4709 print('Closing issue '
4710 '(you may be prompted for your codereview password)...')
4711 cl.UpdateDescription(change_desc.description)
4712 cl.CloseIssue()
4713 props = cl.GetIssueProperties()
4714 patch_num = len(props['patchsets'])
4715 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
4716 patch_num, props['patchsets'][-1], to_pending, revision)
4717 if options.bypass_hooks:
4718 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4719 else:
4720 comment += ' (presubmit successful).'
4721 cl.RpcServer().add_comment(cl.GetIssue(), comment)
4722
4723 if pushed_to_pending:
4724 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
4725 print('The commit is in the pending queue (%s).' % pending_ref)
4726 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4727 'footer.' % branch)
4728
4729 if os.path.isfile(POSTUPSTREAM_HOOK):
4730 RunCommand([POSTUPSTREAM_HOOK, merge_base], error_ok=True)
4731
4732 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004733
4734
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004735@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004736def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004737 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004738 parser.add_option('-b', dest='newbranch',
4739 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004740 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004741 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004742 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4743 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004744 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004745 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004746 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004747 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004748 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004749 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004750
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004751
4752 group = optparse.OptionGroup(
4753 parser,
4754 'Options for continuing work on the current issue uploaded from a '
4755 'different clone (e.g. different machine). Must be used independently '
4756 'from the other options. No issue number should be specified, and the '
4757 'branch must have an issue number associated with it')
4758 group.add_option('--reapply', action='store_true', dest='reapply',
4759 help='Reset the branch and reapply the issue.\n'
4760 'CAUTION: This will undo any local changes in this '
4761 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004762
4763 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004764 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004765 parser.add_option_group(group)
4766
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004767 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004768 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004769 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004770 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004771 auth_config = auth.extract_auth_config_from_options(options)
4772
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004773
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004774 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004775 if options.newbranch:
4776 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004777 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004778 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004779
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004780 cl = Changelist(auth_config=auth_config,
4781 codereview=options.forced_codereview)
4782 if not cl.GetIssue():
4783 parser.error('current branch must have an associated issue')
4784
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004785 upstream = cl.GetUpstreamBranch()
4786 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004787 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004788
4789 RunGit(['reset', '--hard', upstream])
4790 if options.pull:
4791 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004792
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004793 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4794 options.directory)
4795
4796 if len(args) != 1 or not args[0]:
4797 parser.error('Must specify issue number or url')
4798
4799 # We don't want uncommitted changes mixed up with the patch.
4800 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004801 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004802
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004803 if options.newbranch:
4804 if options.force:
4805 RunGit(['branch', '-D', options.newbranch],
4806 stderr=subprocess2.PIPE, error_ok=True)
4807 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004808 elif not GetCurrentBranch():
4809 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004810
4811 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4812
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004813 if cl.IsGerrit():
4814 if options.reject:
4815 parser.error('--reject is not supported with Gerrit codereview.')
4816 if options.nocommit:
4817 parser.error('--nocommit is not supported with Gerrit codereview.')
4818 if options.directory:
4819 parser.error('--directory is not supported with Gerrit codereview.')
4820
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004821 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004822 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004823
4824
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004825def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004826 """Fetches the tree status and returns either 'open', 'closed',
4827 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004828 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004829 if url:
4830 status = urllib2.urlopen(url).read().lower()
4831 if status.find('closed') != -1 or status == '0':
4832 return 'closed'
4833 elif status.find('open') != -1 or status == '1':
4834 return 'open'
4835 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004836 return 'unset'
4837
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004838
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004839def GetTreeStatusReason():
4840 """Fetches the tree status from a json url and returns the message
4841 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004842 url = settings.GetTreeStatusUrl()
4843 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004844 connection = urllib2.urlopen(json_url)
4845 status = json.loads(connection.read())
4846 connection.close()
4847 return status['message']
4848
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004849
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004850def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004851 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004852 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004853 status = GetTreeStatus()
4854 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004855 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004856 return 2
4857
vapiera7fbd5a2016-06-16 09:17:49 -07004858 print('The tree is %s' % status)
4859 print()
4860 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004861 if status != 'open':
4862 return 1
4863 return 0
4864
4865
maruel@chromium.org15192402012-09-06 12:38:29 +00004866def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004867 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004868 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004869 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004870 '-b', '--bot', action='append',
4871 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4872 'times to specify multiple builders. ex: '
4873 '"-b win_rel -b win_layout". See '
4874 'the try server waterfall for the builders name and the tests '
4875 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004876 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004877 '-B', '--bucket', default='',
4878 help=('Buildbucket bucket to send the try requests.'))
4879 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004880 '-m', '--master', default='',
4881 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004882 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004883 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004884 help='Revision to use for the try job; default: the revision will '
4885 'be determined by the try recipe that builder runs, which usually '
4886 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004887 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004888 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004889 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004890 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004891 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004892 '--project',
4893 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004894 'in recipe to determine to which repository or directory to '
4895 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004896 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004897 '-p', '--property', dest='properties', action='append', default=[],
4898 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004899 'key2=value2 etc. The value will be treated as '
4900 'json if decodable, or as string otherwise. '
4901 'NOTE: using this may make your try job not usable for CQ, '
4902 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004903 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004904 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4905 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004906 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004907 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004908 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004909 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004910
machenbach@chromium.org45453142015-09-15 08:45:22 +00004911 # Make sure that all properties are prop=value pairs.
4912 bad_params = [x for x in options.properties if '=' not in x]
4913 if bad_params:
4914 parser.error('Got properties with missing "=": %s' % bad_params)
4915
maruel@chromium.org15192402012-09-06 12:38:29 +00004916 if args:
4917 parser.error('Unknown arguments: %s' % args)
4918
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004919 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004920 if not cl.GetIssue():
4921 parser.error('Need to upload first')
4922
tandriie113dfd2016-10-11 10:20:12 -07004923 error_message = cl.CannotTriggerTryJobReason()
4924 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004925 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004926
borenet6c0efe62016-10-19 08:13:29 -07004927 if options.bucket and options.master:
4928 parser.error('Only one of --bucket and --master may be used.')
4929
qyearsley1fdfcb62016-10-24 13:22:03 -07004930 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004931
qyearsleydd49f942016-10-28 11:57:22 -07004932 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4933 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004934 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004935 if options.verbose:
4936 print('git cl try with no bots now defaults to CQ Dry Run.')
4937 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004938
borenet6c0efe62016-10-19 08:13:29 -07004939 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004940 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004941 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004942 'of bot requires an initial job from a parent (usually a builder). '
4943 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004944 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004945 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004946
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004947 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004948 # TODO(tandrii): Checking local patchset against remote patchset is only
4949 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4950 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07004951 print('Warning: Codereview server has newer patchsets (%s) than most '
4952 'recent upload from local checkout (%s). Did a previous upload '
4953 'fail?\n'
4954 'By default, git cl try uses the latest patchset from '
4955 'codereview, continuing to use patchset %s.\n' %
4956 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07004957
tandrii568043b2016-10-11 07:49:18 -07004958 try:
borenet6c0efe62016-10-19 08:13:29 -07004959 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
4960 patchset)
tandrii568043b2016-10-11 07:49:18 -07004961 except BuildbucketResponseException as ex:
4962 print('ERROR: %s' % ex)
4963 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004964 return 0
4965
4966
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004967def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004968 """Prints info about try jobs associated with current CL."""
4969 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004970 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004971 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004972 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004973 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004974 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004975 '--color', action='store_true', default=setup_color.IS_TTY,
4976 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004977 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004978 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4979 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004980 group.add_option(
4981 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004982 parser.add_option_group(group)
4983 auth.add_auth_options(parser)
4984 options, args = parser.parse_args(args)
4985 if args:
4986 parser.error('Unrecognized args: %s' % ' '.join(args))
4987
4988 auth_config = auth.extract_auth_config_from_options(options)
4989 cl = Changelist(auth_config=auth_config)
4990 if not cl.GetIssue():
4991 parser.error('Need to upload first')
4992
tandrii221ab252016-10-06 08:12:04 -07004993 patchset = options.patchset
4994 if not patchset:
4995 patchset = cl.GetMostRecentPatchset()
4996 if not patchset:
4997 parser.error('Codereview doesn\'t know about issue %s. '
4998 'No access to issue or wrong issue number?\n'
4999 'Either upload first, or pass --patchset explicitely' %
5000 cl.GetIssue())
5001
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005002 # TODO(tandrii): Checking local patchset against remote patchset is only
5003 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5004 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07005005 print('Warning: Codereview server has newer patchsets (%s) than most '
5006 'recent upload from local checkout (%s). Did a previous upload '
5007 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07005008 'By default, git cl try-results uses the latest patchset from '
5009 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07005010 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005011 try:
tandrii221ab252016-10-06 08:12:04 -07005012 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005013 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005014 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005015 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005016 if options.json:
5017 write_try_results_json(options.json, jobs)
5018 else:
5019 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005020 return 0
5021
5022
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005023@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005024def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005025 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005026 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005027 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005028 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005029
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005030 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005031 if args:
5032 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005033 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005034 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005035 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005036 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005037
5038 # Clear configured merge-base, if there is one.
5039 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005040 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005041 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005042 return 0
5043
5044
thestig@chromium.org00858c82013-12-02 23:08:03 +00005045def CMDweb(parser, args):
5046 """Opens the current CL in the web browser."""
5047 _, args = parser.parse_args(args)
5048 if args:
5049 parser.error('Unrecognized args: %s' % ' '.join(args))
5050
5051 issue_url = Changelist().GetIssueURL()
5052 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005053 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005054 return 1
5055
5056 webbrowser.open(issue_url)
5057 return 0
5058
5059
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005060def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005061 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005062 parser.add_option('-d', '--dry-run', action='store_true',
5063 help='trigger in dry run mode')
5064 parser.add_option('-c', '--clear', action='store_true',
5065 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005066 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005067 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005068 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005069 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005070 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005071 if args:
5072 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005073 if options.dry_run and options.clear:
5074 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5075
iannuccie53c9352016-08-17 14:40:40 -07005076 cl = Changelist(auth_config=auth_config, issue=options.issue,
5077 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005078 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005079 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005080 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07005081 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005082 state = _CQState.DRY_RUN
5083 else:
5084 state = _CQState.COMMIT
5085 if not cl.GetIssue():
5086 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005087 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005088 return 0
5089
5090
groby@chromium.org411034a2013-02-26 15:12:01 +00005091def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005092 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005093 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005094 auth.add_auth_options(parser)
5095 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005096 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005097 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005098 if args:
5099 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005100 cl = Changelist(auth_config=auth_config, issue=options.issue,
5101 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005102 # Ensure there actually is an issue to close.
5103 cl.GetDescription()
5104 cl.CloseIssue()
5105 return 0
5106
5107
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005108def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005109 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005110 parser.add_option(
5111 '--stat',
5112 action='store_true',
5113 dest='stat',
5114 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005115 auth.add_auth_options(parser)
5116 options, args = parser.parse_args(args)
5117 auth_config = auth.extract_auth_config_from_options(options)
5118 if args:
5119 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005120
5121 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005122 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005123 # Staged changes would be committed along with the patch from last
5124 # upload, hence counted toward the "last upload" side in the final
5125 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005126 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005127 return 1
5128
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005129 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005130 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005131 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005132 if not issue:
5133 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005134 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005135 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005136
5137 # Create a new branch based on the merge-base
5138 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005139 # Clear cached branch in cl object, to avoid overwriting original CL branch
5140 # properties.
5141 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005142 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005143 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005144 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005145 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005146 return rtn
5147
wychen@chromium.org06928532015-02-03 02:11:29 +00005148 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005149 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005150 cmd = ['git', 'diff']
5151 if options.stat:
5152 cmd.append('--stat')
5153 cmd.extend([TMP_BRANCH, branch, '--'])
5154 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005155 finally:
5156 RunGit(['checkout', '-q', branch])
5157 RunGit(['branch', '-D', TMP_BRANCH])
5158
5159 return 0
5160
5161
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005162def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005163 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005164 parser.add_option(
5165 '--no-color',
5166 action='store_true',
5167 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005168 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005169 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005170 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005171
5172 author = RunGit(['config', 'user.email']).strip() or None
5173
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005174 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005175
5176 if args:
5177 if len(args) > 1:
5178 parser.error('Unknown args')
5179 base_branch = args[0]
5180 else:
5181 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005182 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005183
5184 change = cl.GetChange(base_branch, None)
5185 return owners_finder.OwnersFinder(
5186 [f.LocalPath() for f in
5187 cl.GetChange(base_branch, None).AffectedFiles()],
5188 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005189 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005190 disable_color=options.no_color).run()
5191
5192
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005193def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005194 """Generates a diff command."""
5195 # Generate diff for the current branch's changes.
5196 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
5197 upstream_commit, '--' ]
5198
5199 if args:
5200 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005201 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005202 diff_cmd.append(arg)
5203 else:
5204 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005205
5206 return diff_cmd
5207
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005208def MatchingFileType(file_name, extensions):
5209 """Returns true if the file name ends with one of the given extensions."""
5210 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005211
enne@chromium.org555cfe42014-01-29 18:21:39 +00005212@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005213def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005214 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lam06dba1b2017-01-18 16:39:43 +11005215 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java', '.js']
kylechar58edce22016-06-17 06:07:51 -07005216 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005217 parser.add_option('--full', action='store_true',
5218 help='Reformat the full content of all touched files')
5219 parser.add_option('--dry-run', action='store_true',
5220 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005221 parser.add_option('--python', action='store_true',
5222 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005223 parser.add_option('--diff', action='store_true',
5224 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005225 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005226
Daniel Chengc55eecf2016-12-30 03:11:02 -08005227 # Normalize any remaining args against the current path, so paths relative to
5228 # the current directory are still resolved as expected.
5229 args = [os.path.join(os.getcwd(), arg) for arg in args]
5230
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005231 # git diff generates paths against the root of the repository. Change
5232 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005233 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005234 if rel_base_path:
5235 os.chdir(rel_base_path)
5236
digit@chromium.org29e47272013-05-17 17:01:46 +00005237 # Grab the merge-base commit, i.e. the upstream commit of the current
5238 # branch when it was created or the last time it was rebased. This is
5239 # to cover the case where the user may have called "git fetch origin",
5240 # moving the origin branch to a newer commit, but hasn't rebased yet.
5241 upstream_commit = None
5242 cl = Changelist()
5243 upstream_branch = cl.GetUpstreamBranch()
5244 if upstream_branch:
5245 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5246 upstream_commit = upstream_commit.strip()
5247
5248 if not upstream_commit:
5249 DieWithError('Could not find base commit for this branch. '
5250 'Are you in detached state?')
5251
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005252 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5253 diff_output = RunGit(changed_files_cmd)
5254 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005255 # Filter out files deleted by this CL
5256 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005257
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005258 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5259 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5260 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005261 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005262
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005263 top_dir = os.path.normpath(
5264 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5265
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005266 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5267 # formatted. This is used to block during the presubmit.
5268 return_value = 0
5269
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005270 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005271 # Locate the clang-format binary in the checkout
5272 try:
5273 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005274 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005275 DieWithError(e)
5276
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005277 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005278 cmd = [clang_format_tool]
5279 if not opts.dry_run and not opts.diff:
5280 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005281 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005282 if opts.diff:
5283 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005284 else:
5285 env = os.environ.copy()
5286 env['PATH'] = str(os.path.dirname(clang_format_tool))
5287 try:
5288 script = clang_format.FindClangFormatScriptInChromiumTree(
5289 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005290 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005291 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005292
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005293 cmd = [sys.executable, script, '-p0']
5294 if not opts.dry_run and not opts.diff:
5295 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005296
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005297 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5298 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005299
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005300 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5301 if opts.diff:
5302 sys.stdout.write(stdout)
5303 if opts.dry_run and len(stdout) > 0:
5304 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005305
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005306 # Similar code to above, but using yapf on .py files rather than clang-format
5307 # on C/C++ files
5308 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005309 yapf_tool = gclient_utils.FindExecutable('yapf')
5310 if yapf_tool is None:
5311 DieWithError('yapf not found in PATH')
5312
5313 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005314 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005315 cmd = [yapf_tool]
5316 if not opts.dry_run and not opts.diff:
5317 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005318 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005319 if opts.diff:
5320 sys.stdout.write(stdout)
5321 else:
5322 # TODO(sbc): yapf --lines mode still has some issues.
5323 # https://github.com/google/yapf/issues/154
5324 DieWithError('--python currently only works with --full')
5325
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005326 # Dart's formatter does not have the nice property of only operating on
5327 # modified chunks, so hard code full.
5328 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005329 try:
5330 command = [dart_format.FindDartFmtToolInChromiumTree()]
5331 if not opts.dry_run and not opts.diff:
5332 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005333 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005334
ppi@chromium.org6593d932016-03-03 15:41:15 +00005335 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005336 if opts.dry_run and stdout:
5337 return_value = 2
5338 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005339 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5340 'found in this checkout. Files in other languages are still '
5341 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005342
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005343 # Format GN build files. Always run on full build files for canonical form.
5344 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005345 cmd = ['gn', 'format' ]
5346 if opts.dry_run or opts.diff:
5347 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005348 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005349 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5350 shell=sys.platform == 'win32',
5351 cwd=top_dir)
5352 if opts.dry_run and gn_ret == 2:
5353 return_value = 2 # Not formatted.
5354 elif opts.diff and gn_ret == 2:
5355 # TODO this should compute and print the actual diff.
5356 print("This change has GN build file diff for " + gn_diff_file)
5357 elif gn_ret != 0:
5358 # For non-dry run cases (and non-2 return values for dry-run), a
5359 # nonzero error code indicates a failure, probably because the file
5360 # doesn't parse.
5361 DieWithError("gn format failed on " + gn_diff_file +
5362 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005363
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005364 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005365
5366
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005367@subcommand.usage('<codereview url or issue id>')
5368def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005369 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005370 _, args = parser.parse_args(args)
5371
5372 if len(args) != 1:
5373 parser.print_help()
5374 return 1
5375
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005376 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005377 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005378 parser.print_help()
5379 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005380 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005381
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005382 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005383 output = RunGit(['config', '--local', '--get-regexp',
5384 r'branch\..*\.%s' % issueprefix],
5385 error_ok=True)
5386 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005387 if issue == target_issue:
5388 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005389
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005390 branches = []
5391 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005392 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005393 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005394 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005395 return 1
5396 if len(branches) == 1:
5397 RunGit(['checkout', branches[0]])
5398 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005399 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005400 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005401 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005402 which = raw_input('Choose by index: ')
5403 try:
5404 RunGit(['checkout', branches[int(which)]])
5405 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005406 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005407 return 1
5408
5409 return 0
5410
5411
maruel@chromium.org29404b52014-09-08 22:58:00 +00005412def CMDlol(parser, args):
5413 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005414 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005415 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5416 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5417 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005418 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005419 return 0
5420
5421
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005422class OptionParser(optparse.OptionParser):
5423 """Creates the option parse and add --verbose support."""
5424 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005425 optparse.OptionParser.__init__(
5426 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005427 self.add_option(
5428 '-v', '--verbose', action='count', default=0,
5429 help='Use 2 times for more debugging info')
5430
5431 def parse_args(self, args=None, values=None):
5432 options, args = optparse.OptionParser.parse_args(self, args, values)
5433 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005434 logging.basicConfig(
5435 level=levels[min(options.verbose, len(levels) - 1)],
5436 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5437 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005438 return options, args
5439
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005440
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005441def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005442 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005443 print('\nYour python version %s is unsupported, please upgrade.\n' %
5444 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005445 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005446
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005447 # Reload settings.
5448 global settings
5449 settings = Settings()
5450
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005451 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005452 dispatcher = subcommand.CommandDispatcher(__name__)
5453 try:
5454 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005455 except auth.AuthenticationError as e:
5456 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005457 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005458 if e.code != 500:
5459 raise
5460 DieWithError(
5461 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5462 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005463 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005464
5465
5466if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005467 # These affect sys.stdout so do it outside of main() to simplify mocks in
5468 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005469 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005470 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005471 try:
5472 sys.exit(main(sys.argv[1:]))
5473 except KeyboardInterrupt:
5474 sys.stderr.write('interrupted\n')
5475 sys.exit(1)