blob: 1700dcf60b28efe53cb9018385d064b8e7d2d08c [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
Christopher Lamf732cd52017-01-24 12:40:11 +110095def DieWithError(message, change_desc=None):
96 if change_desc:
97 SaveDescriptionBackup(change_desc)
98
vapiera7fbd5a2016-06-16 09:17:49 -070099 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000100 sys.exit(1)
101
102
Christopher Lamf732cd52017-01-24 12:40:11 +1100103def SaveDescriptionBackup(change_desc):
104 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
105 print('\nError after CL description prompt -- saving description to %s\n' %
106 backup_path)
107 backup_file = open(backup_path, 'w')
108 backup_file.write(change_desc.description)
109 backup_file.close()
110
111
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000112def GetNoGitPagerEnv():
113 env = os.environ.copy()
114 # 'cat' is a magical git string that disables pagers on all platforms.
115 env['GIT_PAGER'] = 'cat'
116 return env
117
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000118
bsep@chromium.org627d9002016-04-29 00:00:52 +0000119def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000120 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000121 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000122 except subprocess2.CalledProcessError as e:
123 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000124 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000125 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000126 'Command "%s" failed.\n%s' % (
127 ' '.join(args), error_message or e.stdout or ''))
128 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000129
130
131def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000132 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000133 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000134
135
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000136def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000137 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700138 if suppress_stderr:
139 stderr = subprocess2.VOID
140 else:
141 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000142 try:
tandrii5d48c322016-08-18 16:19:37 -0700143 (out, _), code = subprocess2.communicate(['git'] + args,
144 env=GetNoGitPagerEnv(),
145 stdout=subprocess2.PIPE,
146 stderr=stderr)
147 return code, out
148 except subprocess2.CalledProcessError as e:
149 logging.debug('Failed running %s', args)
150 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000151
152
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000153def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000154 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000155 return RunGitWithCode(args, suppress_stderr=True)[1]
156
157
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000158def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000159 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000160 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000161 return (version.startswith(prefix) and
162 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000163
164
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000165def BranchExists(branch):
166 """Return True if specified branch exists."""
167 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
168 suppress_stderr=True)
169 return not code
170
171
tandrii2a16b952016-10-19 07:09:44 -0700172def time_sleep(seconds):
173 # Use this so that it can be mocked in tests without interfering with python
174 # system machinery.
175 import time # Local import to discourage others from importing time globally.
176 return time.sleep(seconds)
177
178
maruel@chromium.org90541732011-04-01 17:54:18 +0000179def ask_for_data(prompt):
180 try:
181 return raw_input(prompt)
182 except KeyboardInterrupt:
183 # Hide the exception.
184 sys.exit(1)
185
186
tandrii5d48c322016-08-18 16:19:37 -0700187def _git_branch_config_key(branch, key):
188 """Helper method to return Git config key for a branch."""
189 assert branch, 'branch name is required to set git config for it'
190 return 'branch.%s.%s' % (branch, key)
191
192
193def _git_get_branch_config_value(key, default=None, value_type=str,
194 branch=False):
195 """Returns git config value of given or current branch if any.
196
197 Returns default in all other cases.
198 """
199 assert value_type in (int, str, bool)
200 if branch is False: # Distinguishing default arg value from None.
201 branch = GetCurrentBranch()
202
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000203 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700204 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000205
tandrii5d48c322016-08-18 16:19:37 -0700206 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700207 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700208 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700209 # git config also has --int, but apparently git config suffers from integer
210 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700211 args.append(_git_branch_config_key(branch, key))
212 code, out = RunGitWithCode(args)
213 if code == 0:
214 value = out.strip()
215 if value_type == int:
216 return int(value)
217 if value_type == bool:
218 return bool(value.lower() == 'true')
219 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000220 return default
221
222
tandrii5d48c322016-08-18 16:19:37 -0700223def _git_set_branch_config_value(key, value, branch=None, **kwargs):
224 """Sets the value or unsets if it's None of a git branch config.
225
226 Valid, though not necessarily existing, branch must be provided,
227 otherwise currently checked out branch is used.
228 """
229 if not branch:
230 branch = GetCurrentBranch()
231 assert branch, 'a branch name OR currently checked out branch is required'
232 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700233 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700234 if value is None:
235 args.append('--unset')
236 elif isinstance(value, bool):
237 args.append('--bool')
238 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700239 else:
tandrii33a46ff2016-08-23 05:53:40 -0700240 # git config also has --int, but apparently git config suffers from integer
241 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700242 value = str(value)
243 args.append(_git_branch_config_key(branch, key))
244 if value is not None:
245 args.append(value)
246 RunGit(args, **kwargs)
247
248
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100249def _get_committer_timestamp(commit):
250 """Returns unix timestamp as integer of a committer in a commit.
251
252 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
253 """
254 # Git also stores timezone offset, but it only affects visual display,
255 # actual point in time is defined by this timestamp only.
256 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
257
258
259def _git_amend_head(message, committer_timestamp):
260 """Amends commit with new message and desired committer_timestamp.
261
262 Sets committer timezone to UTC.
263 """
264 env = os.environ.copy()
265 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
266 return RunGit(['commit', '--amend', '-m', message], env=env)
267
268
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000269def add_git_similarity(parser):
270 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700271 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000272 help='Sets the percentage that a pair of files need to match in order to'
273 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000274 parser.add_option(
275 '--find-copies', action='store_true',
276 help='Allows git to look for copies.')
277 parser.add_option(
278 '--no-find-copies', action='store_false', dest='find_copies',
279 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000280
281 old_parser_args = parser.parse_args
282 def Parse(args):
283 options, args = old_parser_args(args)
284
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000285 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700286 options.similarity = _git_get_branch_config_value(
287 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000288 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000289 print('Note: Saving similarity of %d%% in git config.'
290 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700291 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000292
iannucci@chromium.org79540052012-10-19 23:15:26 +0000293 options.similarity = max(0, min(options.similarity, 100))
294
295 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700296 options.find_copies = _git_get_branch_config_value(
297 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000298 else:
tandrii5d48c322016-08-18 16:19:37 -0700299 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000300
301 print('Using %d%% similarity for rename/copy detection. '
302 'Override with --similarity.' % options.similarity)
303
304 return options, args
305 parser.parse_args = Parse
306
307
machenbach@chromium.org45453142015-09-15 08:45:22 +0000308def _get_properties_from_options(options):
309 properties = dict(x.split('=', 1) for x in options.properties)
310 for key, val in properties.iteritems():
311 try:
312 properties[key] = json.loads(val)
313 except ValueError:
314 pass # If a value couldn't be evaluated, treat it as a string.
315 return properties
316
317
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000318def _prefix_master(master):
319 """Convert user-specified master name to full master name.
320
321 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
322 name, while the developers always use shortened master name
323 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
324 function does the conversion for buildbucket migration.
325 """
borenet6c0efe62016-10-19 08:13:29 -0700326 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000327 return master
borenet6c0efe62016-10-19 08:13:29 -0700328 return '%s%s' % (MASTER_PREFIX, master)
329
330
331def _unprefix_master(bucket):
332 """Convert bucket name to shortened master name.
333
334 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
335 name, while the developers always use shortened master name
336 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
337 function does the conversion for buildbucket migration.
338 """
339 if bucket.startswith(MASTER_PREFIX):
340 return bucket[len(MASTER_PREFIX):]
341 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000342
343
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000344def _buildbucket_retry(operation_name, http, *args, **kwargs):
345 """Retries requests to buildbucket service and returns parsed json content."""
346 try_count = 0
347 while True:
348 response, content = http.request(*args, **kwargs)
349 try:
350 content_json = json.loads(content)
351 except ValueError:
352 content_json = None
353
354 # Buildbucket could return an error even if status==200.
355 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000356 error = content_json.get('error')
357 if error.get('code') == 403:
358 raise BuildbucketResponseException(
359 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000360 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000361 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000362 raise BuildbucketResponseException(msg)
363
364 if response.status == 200:
365 if not content_json:
366 raise BuildbucketResponseException(
367 'Buildbucket returns invalid json content: %s.\n'
368 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
369 content)
370 return content_json
371 if response.status < 500 or try_count >= 2:
372 raise httplib2.HttpLib2Error(content)
373
374 # status >= 500 means transient failures.
375 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700376 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000377 try_count += 1
378 assert False, 'unreachable'
379
380
qyearsley1fdfcb62016-10-24 13:22:03 -0700381def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700382 """Returns a dict mapping bucket names to builders and tests,
383 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700384 """
qyearsleydd49f942016-10-28 11:57:22 -0700385 # If no bots are listed, we try to get a set of builders and tests based
386 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700387 if not options.bot:
388 change = changelist.GetChange(
389 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700390 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700391 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700392 change=change,
393 changed_files=change.LocalPaths(),
394 repository_root=settings.GetRoot(),
395 default_presubmit=None,
396 project=None,
397 verbose=options.verbose,
398 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700399 if masters is None:
400 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100401 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700402
qyearsley1fdfcb62016-10-24 13:22:03 -0700403 if options.bucket:
404 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700405 if options.master:
406 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700407
qyearsleydd49f942016-10-28 11:57:22 -0700408 # If bots are listed but no master or bucket, then we need to find out
409 # the corresponding master for each bot.
410 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
411 if error_message:
412 option_parser.error(
413 'Tryserver master cannot be found because: %s\n'
414 'Please manually specify the tryserver master, e.g. '
415 '"-m tryserver.chromium.linux".' % error_message)
416 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700417
418
qyearsley123a4682016-10-26 09:12:17 -0700419def _get_bucket_map_for_builders(builders):
420 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700421 map_url = 'https://builders-map.appspot.com/'
422 try:
qyearsley123a4682016-10-26 09:12:17 -0700423 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700424 except urllib2.URLError as e:
425 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
426 (map_url, e))
427 except ValueError as e:
428 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700429 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700430 return None, 'Failed to build master map.'
431
qyearsley123a4682016-10-26 09:12:17 -0700432 bucket_map = {}
433 for builder in builders:
qyearsley123a4682016-10-26 09:12:17 -0700434 masters = builders_map.get(builder, [])
435 if not masters:
qyearsley1fdfcb62016-10-24 13:22:03 -0700436 return None, ('No matching master for builder %s.' % builder)
qyearsley123a4682016-10-26 09:12:17 -0700437 if len(masters) > 1:
qyearsley1fdfcb62016-10-24 13:22:03 -0700438 return None, ('The builder name %s exists in multiple masters %s.' %
qyearsley123a4682016-10-26 09:12:17 -0700439 (builder, masters))
440 bucket = _prefix_master(masters[0])
441 bucket_map.setdefault(bucket, {})[builder] = []
442
443 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700444
445
borenet6c0efe62016-10-19 08:13:29 -0700446def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700447 category='git_cl_try', patchset=None):
qyearsley1fdfcb62016-10-24 13:22:03 -0700448 """Sends a request to Buildbucket to trigger try jobs for a changelist.
449
450 Args:
451 auth_config: AuthConfig for Rietveld.
452 changelist: Changelist that the try jobs are associated with.
453 buckets: A nested dict mapping bucket names to builders to tests.
454 options: Command-line options.
455 """
tandriide281ae2016-10-12 06:02:30 -0700456 assert changelist.GetIssue(), 'CL must be uploaded first'
457 codereview_url = changelist.GetCodereviewServer()
458 assert codereview_url, 'CL must be uploaded first'
459 patchset = patchset or changelist.GetMostRecentPatchset()
460 assert patchset, 'CL must be uploaded first'
461
462 codereview_host = urlparse.urlparse(codereview_url).hostname
463 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000464 http = authenticator.authorize(httplib2.Http())
465 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700466
467 # TODO(tandrii): consider caching Gerrit CL details just like
468 # _RietveldChangelistImpl does, then caching values in these two variables
469 # won't be necessary.
470 owner_email = changelist.GetIssueOwner()
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000471
472 buildbucket_put_url = (
473 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000474 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700475 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
476 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
477 hostname=codereview_host,
478 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000479 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700480
481 shared_parameters_properties = changelist.GetTryjobProperties(patchset)
482 shared_parameters_properties['category'] = category
483 if options.clobber:
484 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700485 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700486 if extra_properties:
487 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000488
489 batch_req_body = {'builds': []}
490 print_text = []
491 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700492 for bucket, builders_and_tests in sorted(buckets.iteritems()):
493 print_text.append('Bucket: %s' % bucket)
494 master = None
495 if bucket.startswith(MASTER_PREFIX):
496 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000497 for builder, tests in sorted(builders_and_tests.iteritems()):
498 print_text.append(' %s: %s' % (builder, tests))
499 parameters = {
500 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000501 'changes': [{
tandriide281ae2016-10-12 06:02:30 -0700502 'author': {'email': owner_email},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000503 'revision': options.revision,
504 }],
tandrii8c5a3532016-11-04 07:52:02 -0700505 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000506 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000507 if 'presubmit' in builder.lower():
508 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000509 if tests:
510 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700511
512 tags = [
513 'builder:%s' % builder,
514 'buildset:%s' % buildset,
515 'user_agent:git_cl_try',
516 ]
517 if master:
518 parameters['properties']['master'] = master
519 tags.append('master:%s' % master)
520
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000521 batch_req_body['builds'].append(
522 {
523 'bucket': bucket,
524 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000525 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700526 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000527 }
528 )
529
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000530 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700531 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000532 http,
533 buildbucket_put_url,
534 'PUT',
535 body=json.dumps(batch_req_body),
536 headers={'Content-Type': 'application/json'}
537 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000538 print_text.append('To see results here, run: git cl try-results')
539 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700540 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000541
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000542
tandrii221ab252016-10-06 08:12:04 -0700543def fetch_try_jobs(auth_config, changelist, buildbucket_host,
544 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700545 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000546
qyearsley53f48a12016-09-01 10:45:13 -0700547 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000548 """
tandrii221ab252016-10-06 08:12:04 -0700549 assert buildbucket_host
550 assert changelist.GetIssue(), 'CL must be uploaded first'
551 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
552 patchset = patchset or changelist.GetMostRecentPatchset()
553 assert patchset, 'CL must be uploaded first'
554
555 codereview_url = changelist.GetCodereviewServer()
556 codereview_host = urlparse.urlparse(codereview_url).hostname
557 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000558 if authenticator.has_cached_credentials():
559 http = authenticator.authorize(httplib2.Http())
560 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700561 print('Warning: Some results might be missing because %s' %
562 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700563 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000564 http = httplib2.Http()
565
566 http.force_exception_to_status_code = True
567
tandrii221ab252016-10-06 08:12:04 -0700568 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
569 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
570 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000571 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700572 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000573 params = {'tag': 'buildset:%s' % buildset}
574
575 builds = {}
576 while True:
577 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700578 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000579 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700580 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000581 for build in content.get('builds', []):
582 builds[build['id']] = build
583 if 'next_cursor' in content:
584 params['start_cursor'] = content['next_cursor']
585 else:
586 break
587 return builds
588
589
qyearsleyeab3c042016-08-24 09:18:28 -0700590def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000591 """Prints nicely result of fetch_try_jobs."""
592 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700593 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000594 return
595
596 # Make a copy, because we'll be modifying builds dictionary.
597 builds = builds.copy()
598 builder_names_cache = {}
599
600 def get_builder(b):
601 try:
602 return builder_names_cache[b['id']]
603 except KeyError:
604 try:
605 parameters = json.loads(b['parameters_json'])
606 name = parameters['builder_name']
607 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700608 print('WARNING: failed to get builder name for build %s: %s' % (
609 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000610 name = None
611 builder_names_cache[b['id']] = name
612 return name
613
614 def get_bucket(b):
615 bucket = b['bucket']
616 if bucket.startswith('master.'):
617 return bucket[len('master.'):]
618 return bucket
619
620 if options.print_master:
621 name_fmt = '%%-%ds %%-%ds' % (
622 max(len(str(get_bucket(b))) for b in builds.itervalues()),
623 max(len(str(get_builder(b))) for b in builds.itervalues()))
624 def get_name(b):
625 return name_fmt % (get_bucket(b), get_builder(b))
626 else:
627 name_fmt = '%%-%ds' % (
628 max(len(str(get_builder(b))) for b in builds.itervalues()))
629 def get_name(b):
630 return name_fmt % get_builder(b)
631
632 def sort_key(b):
633 return b['status'], b.get('result'), get_name(b), b.get('url')
634
635 def pop(title, f, color=None, **kwargs):
636 """Pop matching builds from `builds` dict and print them."""
637
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000638 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000639 colorize = str
640 else:
641 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
642
643 result = []
644 for b in builds.values():
645 if all(b.get(k) == v for k, v in kwargs.iteritems()):
646 builds.pop(b['id'])
647 result.append(b)
648 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700649 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000650 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700651 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000652
653 total = len(builds)
654 pop(status='COMPLETED', result='SUCCESS',
655 title='Successes:', color=Fore.GREEN,
656 f=lambda b: (get_name(b), b.get('url')))
657 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
658 title='Infra Failures:', color=Fore.MAGENTA,
659 f=lambda b: (get_name(b), b.get('url')))
660 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
661 title='Failures:', color=Fore.RED,
662 f=lambda b: (get_name(b), b.get('url')))
663 pop(status='COMPLETED', result='CANCELED',
664 title='Canceled:', color=Fore.MAGENTA,
665 f=lambda b: (get_name(b),))
666 pop(status='COMPLETED', result='FAILURE',
667 failure_reason='INVALID_BUILD_DEFINITION',
668 title='Wrong master/builder name:', color=Fore.MAGENTA,
669 f=lambda b: (get_name(b),))
670 pop(status='COMPLETED', result='FAILURE',
671 title='Other failures:',
672 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
673 pop(status='COMPLETED',
674 title='Other finished:',
675 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
676 pop(status='STARTED',
677 title='Started:', color=Fore.YELLOW,
678 f=lambda b: (get_name(b), b.get('url')))
679 pop(status='SCHEDULED',
680 title='Scheduled:',
681 f=lambda b: (get_name(b), 'id=%s' % b['id']))
682 # The last section is just in case buildbucket API changes OR there is a bug.
683 pop(title='Other:',
684 f=lambda b: (get_name(b), 'id=%s' % b['id']))
685 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700686 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000687
688
qyearsley53f48a12016-09-01 10:45:13 -0700689def write_try_results_json(output_file, builds):
690 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
691
692 The input |builds| dict is assumed to be generated by Buildbucket.
693 Buildbucket documentation: http://goo.gl/G0s101
694 """
695
696 def convert_build_dict(build):
697 return {
698 'buildbucket_id': build.get('id'),
699 'status': build.get('status'),
700 'result': build.get('result'),
701 'bucket': build.get('bucket'),
702 'builder_name': json.loads(
703 build.get('parameters_json', '{}')).get('builder_name'),
704 'failure_reason': build.get('failure_reason'),
705 'url': build.get('url'),
706 }
707
708 converted = []
709 for _, build in sorted(builds.items()):
710 converted.append(convert_build_dict(build))
711 write_json(output_file, converted)
712
713
iannucci@chromium.org79540052012-10-19 23:15:26 +0000714def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000715 """Prints statistics about the change to the user."""
716 # --no-ext-diff is broken in some versions of Git, so try to work around
717 # this by overriding the environment (but there is still a problem if the
718 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000719 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000720 if 'GIT_EXTERNAL_DIFF' in env:
721 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000722
723 if find_copies:
scottmgb84b5e32016-11-10 09:25:33 -0800724 similarity_options = ['-l100000', '-C%s' % similarity]
iannucci@chromium.org79540052012-10-19 23:15:26 +0000725 else:
726 similarity_options = ['-M%s' % similarity]
727
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000728 try:
729 stdout = sys.stdout.fileno()
730 except AttributeError:
731 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000732 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000733 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000734 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000735 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000736
737
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000738class BuildbucketResponseException(Exception):
739 pass
740
741
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000742class Settings(object):
743 def __init__(self):
744 self.default_server = None
745 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000746 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000747 self.tree_status_url = None
748 self.viewvc_url = None
749 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000750 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000751 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000752 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000753 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000754 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000755 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000756 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000757
758 def LazyUpdateIfNeeded(self):
759 """Updates the settings from a codereview.settings file, if available."""
760 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000761 # The only value that actually changes the behavior is
762 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000763 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000764 error_ok=True
765 ).strip().lower()
766
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000767 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000768 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000769 LoadCodereviewSettingsFromFile(cr_settings_file)
770 self.updated = True
771
772 def GetDefaultServerUrl(self, error_ok=False):
773 if not self.default_server:
774 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000775 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000776 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000777 if error_ok:
778 return self.default_server
779 if not self.default_server:
780 error_message = ('Could not find settings file. You must configure '
781 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000782 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000783 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000784 return self.default_server
785
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000786 @staticmethod
787 def GetRelativeRoot():
788 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000789
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000790 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000791 if self.root is None:
792 self.root = os.path.abspath(self.GetRelativeRoot())
793 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000794
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000795 def GetGitMirror(self, remote='origin'):
796 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000797 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000798 if not os.path.isdir(local_url):
799 return None
800 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
801 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
802 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
803 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
804 if mirror.exists():
805 return mirror
806 return None
807
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000808 def GetTreeStatusUrl(self, error_ok=False):
809 if not self.tree_status_url:
810 error_message = ('You must configure your tree status URL by running '
811 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000812 self.tree_status_url = self._GetRietveldConfig(
813 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000814 return self.tree_status_url
815
816 def GetViewVCUrl(self):
817 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000818 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000819 return self.viewvc_url
820
rmistry@google.com90752582014-01-14 21:04:50 +0000821 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000822 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000823
rmistry@google.com78948ed2015-07-08 23:09:57 +0000824 def GetIsSkipDependencyUpload(self, branch_name):
825 """Returns true if specified branch should skip dep uploads."""
826 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
827 error_ok=True)
828
rmistry@google.com5626a922015-02-26 14:03:30 +0000829 def GetRunPostUploadHook(self):
830 run_post_upload_hook = self._GetRietveldConfig(
831 'run-post-upload-hook', error_ok=True)
832 return run_post_upload_hook == "True"
833
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000834 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000835 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000836
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000837 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000838 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000839
ukai@chromium.orge8077812012-02-03 03:41:46 +0000840 def GetIsGerrit(self):
841 """Return true if this repo is assosiated with gerrit code review system."""
842 if self.is_gerrit is None:
843 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
844 return self.is_gerrit
845
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000846 def GetSquashGerritUploads(self):
847 """Return true if uploads to Gerrit should be squashed by default."""
848 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700849 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
850 if self.squash_gerrit_uploads is None:
851 # Default is squash now (http://crbug.com/611892#c23).
852 self.squash_gerrit_uploads = not (
853 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
854 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000855 return self.squash_gerrit_uploads
856
tandriia60502f2016-06-20 02:01:53 -0700857 def GetSquashGerritUploadsOverride(self):
858 """Return True or False if codereview.settings should be overridden.
859
860 Returns None if no override has been defined.
861 """
862 # See also http://crbug.com/611892#c23
863 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
864 error_ok=True).strip()
865 if result == 'true':
866 return True
867 if result == 'false':
868 return False
869 return None
870
tandrii@chromium.org28253532016-04-14 13:46:56 +0000871 def GetGerritSkipEnsureAuthenticated(self):
872 """Return True if EnsureAuthenticated should not be done for Gerrit
873 uploads."""
874 if self.gerrit_skip_ensure_authenticated is None:
875 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000876 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000877 error_ok=True).strip() == 'true')
878 return self.gerrit_skip_ensure_authenticated
879
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000880 def GetGitEditor(self):
881 """Return the editor specified in the git config, or None if none is."""
882 if self.git_editor is None:
883 self.git_editor = self._GetConfig('core.editor', error_ok=True)
884 return self.git_editor or None
885
thestig@chromium.org44202a22014-03-11 19:22:18 +0000886 def GetLintRegex(self):
887 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
888 DEFAULT_LINT_REGEX)
889
890 def GetLintIgnoreRegex(self):
891 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
892 DEFAULT_LINT_IGNORE_REGEX)
893
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000894 def GetProject(self):
895 if not self.project:
896 self.project = self._GetRietveldConfig('project', error_ok=True)
897 return self.project
898
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000899 def GetPendingRefPrefix(self):
900 if not self.pending_ref_prefix:
901 self.pending_ref_prefix = self._GetRietveldConfig(
902 'pending-ref-prefix', error_ok=True)
903 return self.pending_ref_prefix
904
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000905 def _GetRietveldConfig(self, param, **kwargs):
906 return self._GetConfig('rietveld.' + param, **kwargs)
907
rmistry@google.com78948ed2015-07-08 23:09:57 +0000908 def _GetBranchConfig(self, branch_name, param, **kwargs):
909 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
910
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000911 def _GetConfig(self, param, **kwargs):
912 self.LazyUpdateIfNeeded()
913 return RunGit(['config', param], **kwargs).strip()
914
915
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100916class _GitNumbererState(object):
917 KNOWN_PROJECTS_WHITELIST = [
918 'chromium/src',
919 'external/webrtc',
920 'v8/v8',
921 ]
922
923 @classmethod
924 def load(cls, remote_url, remote_ref):
925 """Figures out the state by fetching special refs from remote repo.
926 """
927 assert remote_ref and remote_ref.startswith('refs/'), remote_ref
928 url_parts = urlparse.urlparse(remote_url)
929 project_name = url_parts.path.lstrip('/').rstrip('git./')
930 for known in cls.KNOWN_PROJECTS_WHITELIST:
931 if project_name.endswith(known):
932 break
933 else:
934 # Early exit to avoid extra fetches for repos that aren't using gnumbd.
935 return cls(cls._get_pending_prefix_fallback(), None)
936
Quinten Yearsley442fb642016-12-15 15:38:27 -0800937 # This pollutes local ref space, but the amount of objects is negligible.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100938 error, _ = cls._run_git_with_code([
939 'fetch', remote_url,
940 '+refs/meta/config:refs/git_cl/meta/config',
941 '+refs/gnumbd-config/main:refs/git_cl/gnumbd-config/main'])
942 if error:
943 # Some ref doesn't exist or isn't accessible to current user.
944 # This shouldn't happen on production KNOWN_PROJECTS_WHITELIST
945 # with git-numberer.
946 cls._warn('failed to fetch gnumbd and project config for %s: %s',
947 remote_url, error)
948 return cls(cls._get_pending_prefix_fallback(), None)
949 return cls(cls._get_pending_prefix(remote_ref),
950 cls._is_validator_enabled(remote_ref))
951
952 @classmethod
953 def _get_pending_prefix(cls, ref):
954 error, gnumbd_config_data = cls._run_git_with_code(
955 ['show', 'refs/git_cl/gnumbd-config/main:config.json'])
956 if error:
957 cls._warn('gnumbd config file not found')
958 return cls._get_pending_prefix_fallback()
959
960 try:
961 config = json.loads(gnumbd_config_data)
962 if cls.match_refglobs(ref, config['enabled_refglobs']):
963 return config['pending_ref_prefix']
964 return None
965 except KeyboardInterrupt:
966 raise
967 except Exception as e:
968 cls._warn('failed to parse gnumbd config: %s', e)
969 return cls._get_pending_prefix_fallback()
970
971 @staticmethod
972 def _get_pending_prefix_fallback():
973 global settings
974 if not settings:
975 settings = Settings()
976 return settings.GetPendingRefPrefix()
977
978 @classmethod
979 def _is_validator_enabled(cls, ref):
980 error, project_config_data = cls._run_git_with_code(
981 ['show', 'refs/git_cl/meta/config:project.config'])
982 if error:
983 cls._warn('project.config file not found')
984 return False
985 # Gerrit's project.config is really a git config file.
986 # So, parse it as such.
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000987 with gclient_utils.temporary_directory() as tempdir:
988 project_config_file = os.path.join(tempdir, 'project.config')
989 gclient_utils.FileWrite(project_config_file, project_config_data)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100990
991 def get_opts(x):
992 code, out = cls._run_git_with_code(
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000993 ['config', '-f', project_config_file, '--get-all',
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100994 'plugin.git-numberer.validate-%s-refglob' % x])
995 if code == 0:
996 return out.strip().splitlines()
997 return []
998 enabled, disabled = map(get_opts, ['enabled', 'disabled'])
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000999
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01001000 logging.info('validator config enabled %s disabled %s refglobs for '
1001 '(this ref: %s)', enabled, disabled, ref)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001002
1003 if cls.match_refglobs(ref, disabled):
1004 return False
1005 return cls.match_refglobs(ref, enabled)
1006
1007 @staticmethod
1008 def match_refglobs(ref, refglobs):
1009 for refglob in refglobs:
1010 if ref == refglob or fnmatch.fnmatch(ref, refglob):
1011 return True
1012 return False
1013
1014 @staticmethod
1015 def _run_git_with_code(*args, **kwargs):
1016 # The only reason for this wrapper is easy porting of this code to CQ
1017 # codebase, which forked git_cl.py and checkouts.py long time ago.
1018 return RunGitWithCode(*args, **kwargs)
1019
1020 @staticmethod
1021 def _warn(msg, *args):
1022 if args:
1023 msg = msg % args
1024 print('WARNING: %s' % msg)
1025
1026 def __init__(self, pending_prefix, validator_enabled):
1027 # TODO(tandrii): remove pending_prefix after gnumbd is no more.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01001028 if pending_prefix:
1029 if not pending_prefix.endswith('/'):
1030 pending_prefix += '/'
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001031 self._pending_prefix = pending_prefix or None
1032 self._validator_enabled = validator_enabled or False
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01001033 logging.debug('_GitNumbererState(pending: %s, validator: %s)',
1034 self._pending_prefix, self._validator_enabled)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001035
1036 @property
1037 def pending_prefix(self):
1038 return self._pending_prefix
1039
1040 @property
Andrii Shyshkalov8f15f3e2016-12-14 15:43:49 +01001041 def should_add_git_number(self):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001042 return self._validator_enabled and self._pending_prefix is None
1043
1044
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001045def ShortBranchName(branch):
1046 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001047 return branch.replace('refs/heads/', '', 1)
1048
1049
1050def GetCurrentBranchRef():
1051 """Returns branch ref (e.g., refs/heads/master) or None."""
1052 return RunGit(['symbolic-ref', 'HEAD'],
1053 stderr=subprocess2.VOID, error_ok=True).strip() or None
1054
1055
1056def GetCurrentBranch():
1057 """Returns current branch or None.
1058
1059 For refs/heads/* branches, returns just last part. For others, full ref.
1060 """
1061 branchref = GetCurrentBranchRef()
1062 if branchref:
1063 return ShortBranchName(branchref)
1064 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001065
1066
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001067class _CQState(object):
1068 """Enum for states of CL with respect to Commit Queue."""
1069 NONE = 'none'
1070 DRY_RUN = 'dry_run'
1071 COMMIT = 'commit'
1072
1073 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1074
1075
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001076class _ParsedIssueNumberArgument(object):
1077 def __init__(self, issue=None, patchset=None, hostname=None):
1078 self.issue = issue
1079 self.patchset = patchset
1080 self.hostname = hostname
1081
1082 @property
1083 def valid(self):
1084 return self.issue is not None
1085
1086
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001087def ParseIssueNumberArgument(arg):
1088 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1089 fail_result = _ParsedIssueNumberArgument()
1090
1091 if arg.isdigit():
1092 return _ParsedIssueNumberArgument(issue=int(arg))
1093 if not arg.startswith('http'):
1094 return fail_result
1095 url = gclient_utils.UpgradeToHttps(arg)
1096 try:
1097 parsed_url = urlparse.urlparse(url)
1098 except ValueError:
1099 return fail_result
1100 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
1101 tmp = cls.ParseIssueURL(parsed_url)
1102 if tmp is not None:
1103 return tmp
1104 return fail_result
1105
1106
Aaron Gablea45ee112016-11-22 15:14:38 -08001107class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001108 def __init__(self, issue, url):
1109 self.issue = issue
1110 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001111 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001112
1113 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001114 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001115 self.issue, self.url)
1116
1117
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001118class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001119 """Changelist works with one changelist in local branch.
1120
1121 Supports two codereview backends: Rietveld or Gerrit, selected at object
1122 creation.
1123
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001124 Notes:
1125 * Not safe for concurrent multi-{thread,process} use.
1126 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001127 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001128 """
1129
1130 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1131 """Create a new ChangeList instance.
1132
1133 If issue is given, the codereview must be given too.
1134
1135 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1136 Otherwise, it's decided based on current configuration of the local branch,
1137 with default being 'rietveld' for backwards compatibility.
1138 See _load_codereview_impl for more details.
1139
1140 **kwargs will be passed directly to codereview implementation.
1141 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001142 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001143 global settings
1144 if not settings:
1145 # Happens when git_cl.py is used as a utility library.
1146 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001147
1148 if issue:
1149 assert codereview, 'codereview must be known, if issue is known'
1150
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001151 self.branchref = branchref
1152 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001153 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001154 self.branch = ShortBranchName(self.branchref)
1155 else:
1156 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001157 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001158 self.lookedup_issue = False
1159 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001160 self.has_description = False
1161 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001162 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001163 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001164 self.cc = None
1165 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001166 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001167
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001168 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001169 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001170 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001171 assert self._codereview_impl
1172 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001173
1174 def _load_codereview_impl(self, codereview=None, **kwargs):
1175 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001176 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1177 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1178 self._codereview = codereview
1179 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001180 return
1181
1182 # Automatic selection based on issue number set for a current branch.
1183 # Rietveld takes precedence over Gerrit.
1184 assert not self.issue
1185 # Whether we find issue or not, we are doing the lookup.
1186 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001187 if self.GetBranch():
1188 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1189 issue = _git_get_branch_config_value(
1190 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1191 if issue:
1192 self._codereview = codereview
1193 self._codereview_impl = cls(self, **kwargs)
1194 self.issue = int(issue)
1195 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001196
1197 # No issue is set for this branch, so decide based on repo-wide settings.
1198 return self._load_codereview_impl(
1199 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1200 **kwargs)
1201
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001202 def IsGerrit(self):
1203 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001204
1205 def GetCCList(self):
1206 """Return the users cc'd on this CL.
1207
agable92bec4f2016-08-24 09:27:27 -07001208 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001209 """
1210 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001211 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001212 more_cc = ','.join(self.watchers)
1213 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1214 return self.cc
1215
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001216 def GetCCListWithoutDefault(self):
1217 """Return the users cc'd on this CL excluding default ones."""
1218 if self.cc is None:
1219 self.cc = ','.join(self.watchers)
1220 return self.cc
1221
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001222 def SetWatchers(self, watchers):
1223 """Set the list of email addresses that should be cc'd based on the changed
1224 files in this CL.
1225 """
1226 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001227
1228 def GetBranch(self):
1229 """Returns the short branch name, e.g. 'master'."""
1230 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001231 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001232 if not branchref:
1233 return None
1234 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001235 self.branch = ShortBranchName(self.branchref)
1236 return self.branch
1237
1238 def GetBranchRef(self):
1239 """Returns the full branch name, e.g. 'refs/heads/master'."""
1240 self.GetBranch() # Poke the lazy loader.
1241 return self.branchref
1242
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001243 def ClearBranch(self):
1244 """Clears cached branch data of this object."""
1245 self.branch = self.branchref = None
1246
tandrii5d48c322016-08-18 16:19:37 -07001247 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1248 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1249 kwargs['branch'] = self.GetBranch()
1250 return _git_get_branch_config_value(key, default, **kwargs)
1251
1252 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1253 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1254 assert self.GetBranch(), (
1255 'this CL must have an associated branch to %sset %s%s' %
1256 ('un' if value is None else '',
1257 key,
1258 '' if value is None else ' to %r' % value))
1259 kwargs['branch'] = self.GetBranch()
1260 return _git_set_branch_config_value(key, value, **kwargs)
1261
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001262 @staticmethod
1263 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001264 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001265 e.g. 'origin', 'refs/heads/master'
1266 """
1267 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001268 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1269
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001270 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001271 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001272 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001273 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1274 error_ok=True).strip()
1275 if upstream_branch:
1276 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001277 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001278 # Else, try to guess the origin remote.
1279 remote_branches = RunGit(['branch', '-r']).split()
1280 if 'origin/master' in remote_branches:
1281 # Fall back on origin/master if it exits.
1282 remote = 'origin'
1283 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001284 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001285 DieWithError(
1286 'Unable to determine default branch to diff against.\n'
1287 'Either pass complete "git diff"-style arguments, like\n'
1288 ' git cl upload origin/master\n'
1289 'or verify this branch is set up to track another \n'
1290 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001291
1292 return remote, upstream_branch
1293
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001294 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001295 upstream_branch = self.GetUpstreamBranch()
1296 if not BranchExists(upstream_branch):
1297 DieWithError('The upstream for the current branch (%s) does not exist '
1298 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001299 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001300 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001301
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001302 def GetUpstreamBranch(self):
1303 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001304 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001305 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001306 upstream_branch = upstream_branch.replace('refs/heads/',
1307 'refs/remotes/%s/' % remote)
1308 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1309 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001310 self.upstream_branch = upstream_branch
1311 return self.upstream_branch
1312
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001313 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001314 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001315 remote, branch = None, self.GetBranch()
1316 seen_branches = set()
1317 while branch not in seen_branches:
1318 seen_branches.add(branch)
1319 remote, branch = self.FetchUpstreamTuple(branch)
1320 branch = ShortBranchName(branch)
1321 if remote != '.' or branch.startswith('refs/remotes'):
1322 break
1323 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001324 remotes = RunGit(['remote'], error_ok=True).split()
1325 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001326 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001327 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001328 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001329 logging.warn('Could not determine which remote this change is '
1330 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001331 else:
1332 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001333 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001334 branch = 'HEAD'
1335 if branch.startswith('refs/remotes'):
1336 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001337 elif branch.startswith('refs/branch-heads/'):
1338 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001339 else:
1340 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001341 return self._remote
1342
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001343 def GitSanityChecks(self, upstream_git_obj):
1344 """Checks git repo status and ensures diff is from local commits."""
1345
sbc@chromium.org79706062015-01-14 21:18:12 +00001346 if upstream_git_obj is None:
1347 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001348 print('ERROR: unable to determine current branch (detached HEAD?)',
1349 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001350 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001351 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001352 return False
1353
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001354 # Verify the commit we're diffing against is in our current branch.
1355 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1356 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1357 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001358 print('ERROR: %s is not in the current branch. You may need to rebase '
1359 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001360 return False
1361
1362 # List the commits inside the diff, and verify they are all local.
1363 commits_in_diff = RunGit(
1364 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1365 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1366 remote_branch = remote_branch.strip()
1367 if code != 0:
1368 _, remote_branch = self.GetRemoteBranch()
1369
1370 commits_in_remote = RunGit(
1371 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1372
1373 common_commits = set(commits_in_diff) & set(commits_in_remote)
1374 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001375 print('ERROR: Your diff contains %d commits already in %s.\n'
1376 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1377 'the diff. If you are using a custom git flow, you can override'
1378 ' the reference used for this check with "git config '
1379 'gitcl.remotebranch <git-ref>".' % (
1380 len(common_commits), remote_branch, upstream_git_obj),
1381 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001382 return False
1383 return True
1384
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001385 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001386 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001387
1388 Returns None if it is not set.
1389 """
tandrii5d48c322016-08-18 16:19:37 -07001390 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001391
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001392 def GetRemoteUrl(self):
1393 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1394
1395 Returns None if there is no remote.
1396 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001397 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001398 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1399
1400 # If URL is pointing to a local directory, it is probably a git cache.
1401 if os.path.isdir(url):
1402 url = RunGit(['config', 'remote.%s.url' % remote],
1403 error_ok=True,
1404 cwd=url).strip()
1405 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001406
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001407 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001408 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001409 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001410 self.issue = self._GitGetBranchConfigValue(
1411 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001412 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001413 return self.issue
1414
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001415 def GetIssueURL(self):
1416 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001417 issue = self.GetIssue()
1418 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001419 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001420 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001421
1422 def GetDescription(self, pretty=False):
1423 if not self.has_description:
1424 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001425 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001426 self.has_description = True
1427 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001428 # Set width to 72 columns + 2 space indent.
1429 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001430 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001431 lines = self.description.splitlines()
1432 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001433 return self.description
1434
1435 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001436 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001437 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001438 self.patchset = self._GitGetBranchConfigValue(
1439 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001440 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001441 return self.patchset
1442
1443 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001444 """Set this branch's patchset. If patchset=0, clears the patchset."""
1445 assert self.GetBranch()
1446 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001447 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001448 else:
1449 self.patchset = int(patchset)
1450 self._GitSetBranchConfigValue(
1451 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001452
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001453 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001454 """Set this branch's issue. If issue isn't given, clears the issue."""
1455 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001456 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001457 issue = int(issue)
1458 self._GitSetBranchConfigValue(
1459 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001460 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001461 codereview_server = self._codereview_impl.GetCodereviewServer()
1462 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001463 self._GitSetBranchConfigValue(
1464 self._codereview_impl.CodereviewServerConfigKey(),
1465 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001466 else:
tandrii5d48c322016-08-18 16:19:37 -07001467 # Reset all of these just to be clean.
1468 reset_suffixes = [
1469 'last-upload-hash',
1470 self._codereview_impl.IssueConfigKey(),
1471 self._codereview_impl.PatchsetConfigKey(),
1472 self._codereview_impl.CodereviewServerConfigKey(),
1473 ] + self._PostUnsetIssueProperties()
1474 for prop in reset_suffixes:
1475 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001476 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001477 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001478
dnjba1b0f32016-09-02 12:37:42 -07001479 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001480 if not self.GitSanityChecks(upstream_branch):
1481 DieWithError('\nGit sanity check failure')
1482
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001483 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001484 if not root:
1485 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001486 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001487
1488 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001489 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001490 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001491 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001492 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001493 except subprocess2.CalledProcessError:
1494 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001495 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001496 'This branch probably doesn\'t exist anymore. To reset the\n'
1497 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001498 ' git branch --set-upstream-to origin/master %s\n'
1499 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001500 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001501
maruel@chromium.org52424302012-08-29 15:14:30 +00001502 issue = self.GetIssue()
1503 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001504 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001505 description = self.GetDescription()
1506 else:
1507 # If the change was never uploaded, use the log messages of all commits
1508 # up to the branch point, as git cl upload will prefill the description
1509 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001510 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1511 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001512
1513 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001514 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001515 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001516 name,
1517 description,
1518 absroot,
1519 files,
1520 issue,
1521 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001522 author,
1523 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001524
dsansomee2d6fd92016-09-08 00:10:47 -07001525 def UpdateDescription(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001526 self.description = description
dsansomee2d6fd92016-09-08 00:10:47 -07001527 return self._codereview_impl.UpdateDescriptionRemote(
1528 description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001529
1530 def RunHook(self, committing, may_prompt, verbose, change):
1531 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1532 try:
1533 return presubmit_support.DoPresubmitChecks(change, committing,
1534 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1535 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001536 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1537 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001538 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001539 DieWithError(
1540 ('%s\nMaybe your depot_tools is out of date?\n'
1541 'If all fails, contact maruel@') % e)
1542
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001543 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1544 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001545 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1546 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001547 else:
1548 # Assume url.
1549 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1550 urlparse.urlparse(issue_arg))
1551 if not parsed_issue_arg or not parsed_issue_arg.valid:
1552 DieWithError('Failed to parse issue argument "%s". '
1553 'Must be an issue number or a valid URL.' % issue_arg)
1554 return self._codereview_impl.CMDPatchWithParsedIssue(
1555 parsed_issue_arg, reject, nocommit, directory)
1556
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001557 def CMDUpload(self, options, git_diff_args, orig_args):
1558 """Uploads a change to codereview."""
1559 if git_diff_args:
1560 # TODO(ukai): is it ok for gerrit case?
1561 base_branch = git_diff_args[0]
1562 else:
1563 if self.GetBranch() is None:
1564 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1565
1566 # Default to diffing against common ancestor of upstream branch
1567 base_branch = self.GetCommonAncestorWithUpstream()
1568 git_diff_args = [base_branch, 'HEAD']
1569
1570 # Make sure authenticated to codereview before running potentially expensive
1571 # hooks. It is a fast, best efforts check. Codereview still can reject the
1572 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001573 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001574
1575 # Apply watchlists on upload.
1576 change = self.GetChange(base_branch, None)
1577 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1578 files = [f.LocalPath() for f in change.AffectedFiles()]
1579 if not options.bypass_watchlists:
1580 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1581
1582 if not options.bypass_hooks:
1583 if options.reviewers or options.tbr_owners:
1584 # Set the reviewer list now so that presubmit checks can access it.
1585 change_description = ChangeDescription(change.FullDescriptionText())
1586 change_description.update_reviewers(options.reviewers,
1587 options.tbr_owners,
1588 change)
1589 change.SetDescriptionText(change_description.description)
1590 hook_results = self.RunHook(committing=False,
1591 may_prompt=not options.force,
1592 verbose=options.verbose,
1593 change=change)
1594 if not hook_results.should_continue():
1595 return 1
1596 if not options.reviewers and hook_results.reviewers:
1597 options.reviewers = hook_results.reviewers.split(',')
1598
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001599 # TODO(tandrii): Checking local patchset against remote patchset is only
1600 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1601 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001602 latest_patchset = self.GetMostRecentPatchset()
1603 local_patchset = self.GetPatchset()
1604 if (latest_patchset and local_patchset and
1605 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001606 print('The last upload made from this repository was patchset #%d but '
1607 'the most recent patchset on the server is #%d.'
1608 % (local_patchset, latest_patchset))
1609 print('Uploading will still work, but if you\'ve uploaded to this '
1610 'issue from another machine or branch the patch you\'re '
1611 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001612 ask_for_data('About to upload; enter to confirm.')
1613
1614 print_stats(options.similarity, options.find_copies, git_diff_args)
1615 ret = self.CMDUploadChange(options, git_diff_args, change)
1616 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001617 if options.use_commit_queue:
1618 self.SetCQState(_CQState.COMMIT)
1619 elif options.cq_dry_run:
1620 self.SetCQState(_CQState.DRY_RUN)
1621
tandrii5d48c322016-08-18 16:19:37 -07001622 _git_set_branch_config_value('last-upload-hash',
1623 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001624 # Run post upload hooks, if specified.
1625 if settings.GetRunPostUploadHook():
1626 presubmit_support.DoPostUploadExecuter(
1627 change,
1628 self,
1629 settings.GetRoot(),
1630 options.verbose,
1631 sys.stdout)
1632
1633 # Upload all dependencies if specified.
1634 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001635 print()
1636 print('--dependencies has been specified.')
1637 print('All dependent local branches will be re-uploaded.')
1638 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001639 # Remove the dependencies flag from args so that we do not end up in a
1640 # loop.
1641 orig_args.remove('--dependencies')
1642 ret = upload_branch_deps(self, orig_args)
1643 return ret
1644
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001645 def SetCQState(self, new_state):
1646 """Update the CQ state for latest patchset.
1647
1648 Issue must have been already uploaded and known.
1649 """
1650 assert new_state in _CQState.ALL_STATES
1651 assert self.GetIssue()
1652 return self._codereview_impl.SetCQState(new_state)
1653
qyearsley1fdfcb62016-10-24 13:22:03 -07001654 def TriggerDryRun(self):
1655 """Triggers a dry run and prints a warning on failure."""
1656 # TODO(qyearsley): Either re-use this method in CMDset_commit
1657 # and CMDupload, or change CMDtry to trigger dry runs with
1658 # just SetCQState, and catch keyboard interrupt and other
1659 # errors in that method.
1660 try:
1661 self.SetCQState(_CQState.DRY_RUN)
1662 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1663 return 0
1664 except KeyboardInterrupt:
1665 raise
1666 except:
1667 print('WARNING: failed to trigger CQ Dry Run.\n'
1668 'Either:\n'
1669 ' * your project has no CQ\n'
1670 ' * you don\'t have permission to trigger Dry Run\n'
1671 ' * bug in this code (see stack trace below).\n'
1672 'Consider specifying which bots to trigger manually '
1673 'or asking your project owners for permissions '
1674 'or contacting Chrome Infrastructure team at '
1675 'https://www.chromium.org/infra\n\n')
1676 # Still raise exception so that stack trace is printed.
1677 raise
1678
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001679 # Forward methods to codereview specific implementation.
1680
1681 def CloseIssue(self):
1682 return self._codereview_impl.CloseIssue()
1683
1684 def GetStatus(self):
1685 return self._codereview_impl.GetStatus()
1686
1687 def GetCodereviewServer(self):
1688 return self._codereview_impl.GetCodereviewServer()
1689
tandriide281ae2016-10-12 06:02:30 -07001690 def GetIssueOwner(self):
1691 """Get owner from codereview, which may differ from this checkout."""
1692 return self._codereview_impl.GetIssueOwner()
1693
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001694 def GetApprovingReviewers(self):
1695 return self._codereview_impl.GetApprovingReviewers()
1696
1697 def GetMostRecentPatchset(self):
1698 return self._codereview_impl.GetMostRecentPatchset()
1699
tandriide281ae2016-10-12 06:02:30 -07001700 def CannotTriggerTryJobReason(self):
1701 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1702 return self._codereview_impl.CannotTriggerTryJobReason()
1703
tandrii8c5a3532016-11-04 07:52:02 -07001704 def GetTryjobProperties(self, patchset=None):
1705 """Returns dictionary of properties to launch tryjob."""
1706 return self._codereview_impl.GetTryjobProperties(patchset=patchset)
1707
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001708 def __getattr__(self, attr):
1709 # This is because lots of untested code accesses Rietveld-specific stuff
1710 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001711 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001712 # Note that child method defines __getattr__ as well, and forwards it here,
1713 # because _RietveldChangelistImpl is not cleaned up yet, and given
1714 # deprecation of Rietveld, it should probably be just removed.
1715 # Until that time, avoid infinite recursion by bypassing __getattr__
1716 # of implementation class.
1717 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001718
1719
1720class _ChangelistCodereviewBase(object):
1721 """Abstract base class encapsulating codereview specifics of a changelist."""
1722 def __init__(self, changelist):
1723 self._changelist = changelist # instance of Changelist
1724
1725 def __getattr__(self, attr):
1726 # Forward methods to changelist.
1727 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1728 # _RietveldChangelistImpl to avoid this hack?
1729 return getattr(self._changelist, attr)
1730
1731 def GetStatus(self):
1732 """Apply a rough heuristic to give a simple summary of an issue's review
1733 or CQ status, assuming adherence to a common workflow.
1734
1735 Returns None if no issue for this branch, or specific string keywords.
1736 """
1737 raise NotImplementedError()
1738
1739 def GetCodereviewServer(self):
1740 """Returns server URL without end slash, like "https://codereview.com"."""
1741 raise NotImplementedError()
1742
1743 def FetchDescription(self):
1744 """Fetches and returns description from the codereview server."""
1745 raise NotImplementedError()
1746
tandrii5d48c322016-08-18 16:19:37 -07001747 @classmethod
1748 def IssueConfigKey(cls):
1749 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001750 raise NotImplementedError()
1751
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001752 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001753 def PatchsetConfigKey(cls):
1754 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001755 raise NotImplementedError()
1756
tandrii5d48c322016-08-18 16:19:37 -07001757 @classmethod
1758 def CodereviewServerConfigKey(cls):
1759 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001760 raise NotImplementedError()
1761
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001762 def _PostUnsetIssueProperties(self):
1763 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001764 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001765
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001766 def GetRieveldObjForPresubmit(self):
1767 # This is an unfortunate Rietveld-embeddedness in presubmit.
1768 # For non-Rietveld codereviews, this probably should return a dummy object.
1769 raise NotImplementedError()
1770
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001771 def GetGerritObjForPresubmit(self):
1772 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1773 return None
1774
dsansomee2d6fd92016-09-08 00:10:47 -07001775 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001776 """Update the description on codereview site."""
1777 raise NotImplementedError()
1778
1779 def CloseIssue(self):
1780 """Closes the issue."""
1781 raise NotImplementedError()
1782
1783 def GetApprovingReviewers(self):
1784 """Returns a list of reviewers approving the change.
1785
1786 Note: not necessarily committers.
1787 """
1788 raise NotImplementedError()
1789
1790 def GetMostRecentPatchset(self):
1791 """Returns the most recent patchset number from the codereview site."""
1792 raise NotImplementedError()
1793
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001794 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1795 directory):
1796 """Fetches and applies the issue.
1797
1798 Arguments:
1799 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1800 reject: if True, reject the failed patch instead of switching to 3-way
1801 merge. Rietveld only.
1802 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1803 only.
1804 directory: switch to directory before applying the patch. Rietveld only.
1805 """
1806 raise NotImplementedError()
1807
1808 @staticmethod
1809 def ParseIssueURL(parsed_url):
1810 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1811 failed."""
1812 raise NotImplementedError()
1813
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001814 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001815 """Best effort check that user is authenticated with codereview server.
1816
1817 Arguments:
1818 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001819 refresh: whether to attempt to refresh credentials. Ignored if not
1820 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001821 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001822 raise NotImplementedError()
1823
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001824 def CMDUploadChange(self, options, args, change):
1825 """Uploads a change to codereview."""
1826 raise NotImplementedError()
1827
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001828 def SetCQState(self, new_state):
1829 """Update the CQ state for latest patchset.
1830
1831 Issue must have been already uploaded and known.
1832 """
1833 raise NotImplementedError()
1834
tandriie113dfd2016-10-11 10:20:12 -07001835 def CannotTriggerTryJobReason(self):
1836 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1837 raise NotImplementedError()
1838
tandriide281ae2016-10-12 06:02:30 -07001839 def GetIssueOwner(self):
1840 raise NotImplementedError()
1841
tandrii8c5a3532016-11-04 07:52:02 -07001842 def GetTryjobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001843 raise NotImplementedError()
1844
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001845
1846class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1847 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1848 super(_RietveldChangelistImpl, self).__init__(changelist)
1849 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001850 if not rietveld_server:
1851 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001852
1853 self._rietveld_server = rietveld_server
1854 self._auth_config = auth_config
1855 self._props = None
1856 self._rpc_server = None
1857
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001858 def GetCodereviewServer(self):
1859 if not self._rietveld_server:
1860 # If we're on a branch then get the server potentially associated
1861 # with that branch.
1862 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001863 self._rietveld_server = gclient_utils.UpgradeToHttps(
1864 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001865 if not self._rietveld_server:
1866 self._rietveld_server = settings.GetDefaultServerUrl()
1867 return self._rietveld_server
1868
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001869 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001870 """Best effort check that user is authenticated with Rietveld server."""
1871 if self._auth_config.use_oauth2:
1872 authenticator = auth.get_authenticator_for_host(
1873 self.GetCodereviewServer(), self._auth_config)
1874 if not authenticator.has_cached_credentials():
1875 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001876 if refresh:
1877 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001878
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001879 def FetchDescription(self):
1880 issue = self.GetIssue()
1881 assert issue
1882 try:
1883 return self.RpcServer().get_description(issue).strip()
1884 except urllib2.HTTPError as e:
1885 if e.code == 404:
1886 DieWithError(
1887 ('\nWhile fetching the description for issue %d, received a '
1888 '404 (not found)\n'
1889 'error. It is likely that you deleted this '
1890 'issue on the server. If this is the\n'
1891 'case, please run\n\n'
1892 ' git cl issue 0\n\n'
1893 'to clear the association with the deleted issue. Then run '
1894 'this command again.') % issue)
1895 else:
1896 DieWithError(
1897 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1898 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001899 print('Warning: Failed to retrieve CL description due to network '
1900 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001901 return ''
1902
1903 def GetMostRecentPatchset(self):
1904 return self.GetIssueProperties()['patchsets'][-1]
1905
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001906 def GetIssueProperties(self):
1907 if self._props is None:
1908 issue = self.GetIssue()
1909 if not issue:
1910 self._props = {}
1911 else:
1912 self._props = self.RpcServer().get_issue_properties(issue, True)
1913 return self._props
1914
tandriie113dfd2016-10-11 10:20:12 -07001915 def CannotTriggerTryJobReason(self):
1916 props = self.GetIssueProperties()
1917 if not props:
1918 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1919 if props.get('closed'):
1920 return 'CL %s is closed' % self.GetIssue()
1921 if props.get('private'):
1922 return 'CL %s is private' % self.GetIssue()
1923 return None
1924
tandrii8c5a3532016-11-04 07:52:02 -07001925 def GetTryjobProperties(self, patchset=None):
1926 """Returns dictionary of properties to launch tryjob."""
1927 project = (self.GetIssueProperties() or {}).get('project')
1928 return {
1929 'issue': self.GetIssue(),
1930 'patch_project': project,
1931 'patch_storage': 'rietveld',
1932 'patchset': patchset or self.GetPatchset(),
1933 'rietveld': self.GetCodereviewServer(),
1934 }
1935
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001936 def GetApprovingReviewers(self):
1937 return get_approving_reviewers(self.GetIssueProperties())
1938
tandriide281ae2016-10-12 06:02:30 -07001939 def GetIssueOwner(self):
1940 return (self.GetIssueProperties() or {}).get('owner_email')
1941
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001942 def AddComment(self, message):
1943 return self.RpcServer().add_comment(self.GetIssue(), message)
1944
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001945 def GetStatus(self):
1946 """Apply a rough heuristic to give a simple summary of an issue's review
1947 or CQ status, assuming adherence to a common workflow.
1948
1949 Returns None if no issue for this branch, or one of the following keywords:
1950 * 'error' - error from review tool (including deleted issues)
1951 * 'unsent' - not sent for review
1952 * 'waiting' - waiting for review
1953 * 'reply' - waiting for owner to reply to review
1954 * 'lgtm' - LGTM from at least one approved reviewer
1955 * 'commit' - in the commit queue
1956 * 'closed' - closed
1957 """
1958 if not self.GetIssue():
1959 return None
1960
1961 try:
1962 props = self.GetIssueProperties()
1963 except urllib2.HTTPError:
1964 return 'error'
1965
1966 if props.get('closed'):
1967 # Issue is closed.
1968 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001969 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001970 # Issue is in the commit queue.
1971 return 'commit'
1972
1973 try:
1974 reviewers = self.GetApprovingReviewers()
1975 except urllib2.HTTPError:
1976 return 'error'
1977
1978 if reviewers:
1979 # Was LGTM'ed.
1980 return 'lgtm'
1981
1982 messages = props.get('messages') or []
1983
tandrii9d2c7a32016-06-22 03:42:45 -07001984 # Skip CQ messages that don't require owner's action.
1985 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1986 if 'Dry run:' in messages[-1]['text']:
1987 messages.pop()
1988 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1989 # This message always follows prior messages from CQ,
1990 # so skip this too.
1991 messages.pop()
1992 else:
1993 # This is probably a CQ messages warranting user attention.
1994 break
1995
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001996 if not messages:
1997 # No message was sent.
1998 return 'unsent'
1999 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07002000 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002001 return 'reply'
2002 return 'waiting'
2003
dsansomee2d6fd92016-09-08 00:10:47 -07002004 def UpdateDescriptionRemote(self, description, force=False):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002005 return self.RpcServer().update_description(
2006 self.GetIssue(), self.description)
2007
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002008 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002009 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002010
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002011 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002012 return self.SetFlags({flag: value})
2013
2014 def SetFlags(self, flags):
2015 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002016 """
phajdan.jr68598232016-08-10 03:28:28 -07002017 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002018 try:
tandrii4b233bd2016-07-06 03:50:29 -07002019 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002020 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002021 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002022 if e.code == 404:
2023 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2024 if e.code == 403:
2025 DieWithError(
2026 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002027 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002028 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002029
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002030 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002031 """Returns an upload.RpcServer() to access this review's rietveld instance.
2032 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002033 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002034 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002035 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002036 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002037 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002038
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002039 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002040 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002041 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002042
tandrii5d48c322016-08-18 16:19:37 -07002043 @classmethod
2044 def PatchsetConfigKey(cls):
2045 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002046
tandrii5d48c322016-08-18 16:19:37 -07002047 @classmethod
2048 def CodereviewServerConfigKey(cls):
2049 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002050
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002051 def GetRieveldObjForPresubmit(self):
2052 return self.RpcServer()
2053
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002054 def SetCQState(self, new_state):
2055 props = self.GetIssueProperties()
2056 if props.get('private'):
2057 DieWithError('Cannot set-commit on private issue')
2058
2059 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002060 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002061 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002062 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002063 else:
tandrii4b233bd2016-07-06 03:50:29 -07002064 assert new_state == _CQState.DRY_RUN
2065 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002066
2067
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002068 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2069 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002070 # PatchIssue should never be called with a dirty tree. It is up to the
2071 # caller to check this, but just in case we assert here since the
2072 # consequences of the caller not checking this could be dire.
2073 assert(not git_common.is_dirty_git_tree('apply'))
2074 assert(parsed_issue_arg.valid)
2075 self._changelist.issue = parsed_issue_arg.issue
2076 if parsed_issue_arg.hostname:
2077 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2078
skobes6468b902016-10-24 08:45:10 -07002079 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2080 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2081 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002082 try:
skobes6468b902016-10-24 08:45:10 -07002083 scm_obj.apply_patch(patchset_object)
2084 except Exception as e:
2085 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002086 return 1
2087
2088 # If we had an issue, commit the current state and register the issue.
2089 if not nocommit:
2090 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2091 'patch from issue %(i)s at patchset '
2092 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2093 % {'i': self.GetIssue(), 'p': patchset})])
2094 self.SetIssue(self.GetIssue())
2095 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07002096 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002097 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002098 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002099 return 0
2100
2101 @staticmethod
2102 def ParseIssueURL(parsed_url):
2103 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2104 return None
wychen3c1c1722016-08-04 11:46:36 -07002105 # Rietveld patch: https://domain/<number>/#ps<patchset>
2106 match = re.match(r'/(\d+)/$', parsed_url.path)
2107 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2108 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002109 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002110 issue=int(match.group(1)),
2111 patchset=int(match2.group(1)),
2112 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002113 # Typical url: https://domain/<issue_number>[/[other]]
2114 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2115 if match:
skobes6468b902016-10-24 08:45:10 -07002116 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002117 issue=int(match.group(1)),
2118 hostname=parsed_url.netloc)
2119 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2120 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2121 if match:
skobes6468b902016-10-24 08:45:10 -07002122 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002123 issue=int(match.group(1)),
2124 patchset=int(match.group(2)),
skobes6468b902016-10-24 08:45:10 -07002125 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002126 return None
2127
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002128 def CMDUploadChange(self, options, args, change):
2129 """Upload the patch to Rietveld."""
2130 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2131 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002132 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2133 if options.emulate_svn_auto_props:
2134 upload_args.append('--emulate_svn_auto_props')
2135
2136 change_desc = None
2137
2138 if options.email is not None:
2139 upload_args.extend(['--email', options.email])
2140
2141 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002142 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002143 upload_args.extend(['--title', options.title])
2144 if options.message:
2145 upload_args.extend(['--message', options.message])
2146 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002147 print('This branch is associated with issue %s. '
2148 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002149 else:
nodirca166002016-06-27 10:59:51 -07002150 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002151 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002152 if options.message:
2153 message = options.message
2154 else:
2155 message = CreateDescriptionFromLog(args)
2156 if options.title:
2157 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002158 change_desc = ChangeDescription(message)
2159 if options.reviewers or options.tbr_owners:
2160 change_desc.update_reviewers(options.reviewers,
2161 options.tbr_owners,
2162 change)
2163 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002164 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002165
2166 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002167 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002168 return 1
2169
2170 upload_args.extend(['--message', change_desc.description])
2171 if change_desc.get_reviewers():
2172 upload_args.append('--reviewers=%s' % ','.join(
2173 change_desc.get_reviewers()))
2174 if options.send_mail:
2175 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002176 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002177 upload_args.append('--send_mail')
2178
2179 # We check this before applying rietveld.private assuming that in
2180 # rietveld.cc only addresses which we can send private CLs to are listed
2181 # if rietveld.private is set, and so we should ignore rietveld.cc only
2182 # when --private is specified explicitly on the command line.
2183 if options.private:
2184 logging.warn('rietveld.cc is ignored since private flag is specified. '
2185 'You need to review and add them manually if necessary.')
2186 cc = self.GetCCListWithoutDefault()
2187 else:
2188 cc = self.GetCCList()
2189 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002190 if change_desc.get_cced():
2191 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002192 if cc:
2193 upload_args.extend(['--cc', cc])
2194
2195 if options.private or settings.GetDefaultPrivateFlag() == "True":
2196 upload_args.append('--private')
2197
2198 upload_args.extend(['--git_similarity', str(options.similarity)])
2199 if not options.find_copies:
2200 upload_args.extend(['--git_no_find_copies'])
2201
2202 # Include the upstream repo's URL in the change -- this is useful for
2203 # projects that have their source spread across multiple repos.
2204 remote_url = self.GetGitBaseUrlFromConfig()
2205 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002206 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2207 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2208 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002209 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002210 remote, remote_branch = self.GetRemoteBranch()
2211 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01002212 pending_prefix_check=True,
2213 remote_url=self.GetRemoteUrl())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002214 if target_ref:
2215 upload_args.extend(['--target_ref', target_ref])
2216
2217 # Look for dependent patchsets. See crbug.com/480453 for more details.
2218 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2219 upstream_branch = ShortBranchName(upstream_branch)
2220 if remote is '.':
2221 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002222 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002223 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002224 print()
2225 print('Skipping dependency patchset upload because git config '
2226 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2227 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002228 else:
2229 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002230 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002231 auth_config=auth_config)
2232 branch_cl_issue_url = branch_cl.GetIssueURL()
2233 branch_cl_issue = branch_cl.GetIssue()
2234 branch_cl_patchset = branch_cl.GetPatchset()
2235 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2236 upload_args.extend(
2237 ['--depends_on_patchset', '%s:%s' % (
2238 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002239 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002240 '\n'
2241 'The current branch (%s) is tracking a local branch (%s) with '
2242 'an associated CL.\n'
2243 'Adding %s/#ps%s as a dependency patchset.\n'
2244 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2245 branch_cl_patchset))
2246
2247 project = settings.GetProject()
2248 if project:
2249 upload_args.extend(['--project', project])
2250
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002251 try:
2252 upload_args = ['upload'] + upload_args + args
2253 logging.info('upload.RealMain(%s)', upload_args)
2254 issue, patchset = upload.RealMain(upload_args)
2255 issue = int(issue)
2256 patchset = int(patchset)
2257 except KeyboardInterrupt:
2258 sys.exit(1)
2259 except:
2260 # If we got an exception after the user typed a description for their
2261 # change, back up the description before re-raising.
2262 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002263 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002264 raise
2265
2266 if not self.GetIssue():
2267 self.SetIssue(issue)
2268 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002269 return 0
2270
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002271class _GerritChangelistImpl(_ChangelistCodereviewBase):
2272 def __init__(self, changelist, auth_config=None):
2273 # auth_config is Rietveld thing, kept here to preserve interface only.
2274 super(_GerritChangelistImpl, self).__init__(changelist)
2275 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002276 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002277 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002278 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002279 # Map from change number (issue) to its detail cache.
2280 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002281
2282 def _GetGerritHost(self):
2283 # Lazy load of configs.
2284 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002285 if self._gerrit_host and '.' not in self._gerrit_host:
2286 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2287 # This happens for internal stuff http://crbug.com/614312.
2288 parsed = urlparse.urlparse(self.GetRemoteUrl())
2289 if parsed.scheme == 'sso':
2290 print('WARNING: using non https URLs for remote is likely broken\n'
2291 ' Your current remote is: %s' % self.GetRemoteUrl())
2292 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2293 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002294 return self._gerrit_host
2295
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002296 def _GetGitHost(self):
2297 """Returns git host to be used when uploading change to Gerrit."""
2298 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2299
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002300 def GetCodereviewServer(self):
2301 if not self._gerrit_server:
2302 # If we're on a branch then get the server potentially associated
2303 # with that branch.
2304 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002305 self._gerrit_server = self._GitGetBranchConfigValue(
2306 self.CodereviewServerConfigKey())
2307 if self._gerrit_server:
2308 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002309 if not self._gerrit_server:
2310 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2311 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002312 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002313 parts[0] = parts[0] + '-review'
2314 self._gerrit_host = '.'.join(parts)
2315 self._gerrit_server = 'https://%s' % self._gerrit_host
2316 return self._gerrit_server
2317
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002318 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002319 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002320 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002321
tandrii5d48c322016-08-18 16:19:37 -07002322 @classmethod
2323 def PatchsetConfigKey(cls):
2324 return 'gerritpatchset'
2325
2326 @classmethod
2327 def CodereviewServerConfigKey(cls):
2328 return 'gerritserver'
2329
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002330 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002331 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002332 if settings.GetGerritSkipEnsureAuthenticated():
2333 # For projects with unusual authentication schemes.
2334 # See http://crbug.com/603378.
2335 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002336 # Lazy-loader to identify Gerrit and Git hosts.
2337 if gerrit_util.GceAuthenticator.is_gce():
2338 return
2339 self.GetCodereviewServer()
2340 git_host = self._GetGitHost()
2341 assert self._gerrit_server and self._gerrit_host
2342 cookie_auth = gerrit_util.CookiesAuthenticator()
2343
2344 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2345 git_auth = cookie_auth.get_auth_header(git_host)
2346 if gerrit_auth and git_auth:
2347 if gerrit_auth == git_auth:
2348 return
2349 print((
2350 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2351 ' Check your %s or %s file for credentials of hosts:\n'
2352 ' %s\n'
2353 ' %s\n'
2354 ' %s') %
2355 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2356 git_host, self._gerrit_host,
2357 cookie_auth.get_new_password_message(git_host)))
2358 if not force:
2359 ask_for_data('If you know what you are doing, press Enter to continue, '
2360 'Ctrl+C to abort.')
2361 return
2362 else:
2363 missing = (
2364 [] if gerrit_auth else [self._gerrit_host] +
2365 [] if git_auth else [git_host])
2366 DieWithError('Credentials for the following hosts are required:\n'
2367 ' %s\n'
2368 'These are read from %s (or legacy %s)\n'
2369 '%s' % (
2370 '\n '.join(missing),
2371 cookie_auth.get_gitcookies_path(),
2372 cookie_auth.get_netrc_path(),
2373 cookie_auth.get_new_password_message(git_host)))
2374
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002375 def _PostUnsetIssueProperties(self):
2376 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002377 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002378
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002379 def GetRieveldObjForPresubmit(self):
2380 class ThisIsNotRietveldIssue(object):
2381 def __nonzero__(self):
2382 # This is a hack to make presubmit_support think that rietveld is not
2383 # defined, yet still ensure that calls directly result in a decent
2384 # exception message below.
2385 return False
2386
2387 def __getattr__(self, attr):
2388 print(
2389 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2390 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2391 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2392 'or use Rietveld for codereview.\n'
2393 'See also http://crbug.com/579160.' % attr)
2394 raise NotImplementedError()
2395 return ThisIsNotRietveldIssue()
2396
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002397 def GetGerritObjForPresubmit(self):
2398 return presubmit_support.GerritAccessor(self._GetGerritHost())
2399
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002400 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002401 """Apply a rough heuristic to give a simple summary of an issue's review
2402 or CQ status, assuming adherence to a common workflow.
2403
2404 Returns None if no issue for this branch, or one of the following keywords:
2405 * 'error' - error from review tool (including deleted issues)
2406 * 'unsent' - no reviewers added
2407 * 'waiting' - waiting for review
2408 * 'reply' - waiting for owner to reply to review
Quinten Yearsley442fb642016-12-15 15:38:27 -08002409 * 'not lgtm' - Code-Review disapproval from at least one valid reviewer
tandriic2405f52016-10-10 08:13:15 -07002410 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002411 * 'commit' - in the commit queue
2412 * 'closed' - abandoned
2413 """
2414 if not self.GetIssue():
2415 return None
2416
2417 try:
2418 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002419 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002420 return 'error'
2421
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002422 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002423 return 'closed'
2424
2425 cq_label = data['labels'].get('Commit-Queue', {})
2426 if cq_label:
rmistryc9ebbd22016-10-14 12:35:54 -07002427 votes = cq_label.get('all', [])
2428 highest_vote = 0
2429 for v in votes:
2430 highest_vote = max(highest_vote, v.get('value', 0))
2431 vote_value = str(highest_vote)
2432 if vote_value != '0':
2433 # Add a '+' if the value is not 0 to match the values in the label.
2434 # The cq_label does not have negatives.
2435 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002436 vote_text = cq_label.get('values', {}).get(vote_value, '')
2437 if vote_text.lower() == 'commit':
2438 return 'commit'
2439
2440 lgtm_label = data['labels'].get('Code-Review', {})
2441 if lgtm_label:
2442 if 'rejected' in lgtm_label:
2443 return 'not lgtm'
2444 if 'approved' in lgtm_label:
2445 return 'lgtm'
2446
2447 if not data.get('reviewers', {}).get('REVIEWER', []):
2448 return 'unsent'
2449
2450 messages = data.get('messages', [])
2451 if messages:
2452 owner = data['owner'].get('_account_id')
2453 last_message_author = messages[-1].get('author', {}).get('_account_id')
2454 if owner != last_message_author:
2455 # Some reply from non-owner.
2456 return 'reply'
2457
2458 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002459
2460 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002461 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002462 return data['revisions'][data['current_revision']]['_number']
2463
2464 def FetchDescription(self):
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002465 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002466 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002467 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002468
dsansomee2d6fd92016-09-08 00:10:47 -07002469 def UpdateDescriptionRemote(self, description, force=False):
2470 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2471 if not force:
2472 ask_for_data(
2473 'The description cannot be modified while the issue has a pending '
2474 'unpublished edit. Either publish the edit in the Gerrit web UI '
2475 'or delete it.\n\n'
2476 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2477
2478 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2479 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002480 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002481 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002482
2483 def CloseIssue(self):
2484 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2485
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002486 def GetApprovingReviewers(self):
2487 """Returns a list of reviewers approving the change.
2488
2489 Note: not necessarily committers.
2490 """
2491 raise NotImplementedError()
2492
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002493 def SubmitIssue(self, wait_for_merge=True):
2494 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2495 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002496
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002497 def _GetChangeDetail(self, options=None, issue=None,
2498 no_cache=False):
2499 """Returns details of the issue by querying Gerrit and caching results.
2500
2501 If fresh data is needed, set no_cache=True which will clear cache and
2502 thus new data will be fetched from Gerrit.
2503 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002504 options = options or []
2505 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002506 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002507
2508 # Normalize issue and options for consistent keys in cache.
2509 issue = str(issue)
2510 options = [o.upper() for o in options]
2511
2512 # Check in cache first unless no_cache is True.
2513 if no_cache:
2514 self._detail_cache.pop(issue, None)
2515 else:
2516 options_set = frozenset(options)
2517 for cached_options_set, data in self._detail_cache.get(issue, []):
2518 # Assumption: data fetched before with extra options is suitable
2519 # for return for a smaller set of options.
2520 # For example, if we cached data for
2521 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2522 # and request is for options=[CURRENT_REVISION],
2523 # THEN we can return prior cached data.
2524 if options_set.issubset(cached_options_set):
2525 return data
2526
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002527 try:
2528 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2529 options, ignore_404=False)
2530 except gerrit_util.GerritError as e:
2531 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002532 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002533 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002534
2535 self._detail_cache.setdefault(issue, []).append((frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002536 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002537
agable32978d92016-11-01 12:55:02 -07002538 def _GetChangeCommit(self, issue=None):
2539 issue = issue or self.GetIssue()
2540 assert issue, 'issue is required to query Gerrit'
2541 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2542 if not data:
Aaron Gablea45ee112016-11-22 15:14:38 -08002543 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
agable32978d92016-11-01 12:55:02 -07002544 return data
2545
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002546 def CMDLand(self, force, bypass_hooks, verbose):
2547 if git_common.is_dirty_git_tree('land'):
2548 return 1
tandriid60367b2016-06-22 05:25:12 -07002549 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2550 if u'Commit-Queue' in detail.get('labels', {}):
2551 if not force:
2552 ask_for_data('\nIt seems this repository has a Commit Queue, '
2553 'which can test and land changes for you. '
2554 'Are you sure you wish to bypass it?\n'
2555 'Press Enter to continue, Ctrl+C to abort.')
2556
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002557 differs = True
tandriic4344b52016-08-29 06:04:54 -07002558 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002559 # Note: git diff outputs nothing if there is no diff.
2560 if not last_upload or RunGit(['diff', last_upload]).strip():
2561 print('WARNING: some changes from local branch haven\'t been uploaded')
2562 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002563 if detail['current_revision'] == last_upload:
2564 differs = False
2565 else:
2566 print('WARNING: local branch contents differ from latest uploaded '
2567 'patchset')
2568 if differs:
2569 if not force:
2570 ask_for_data(
Andrii Shyshkalov3aac7752016-11-16 15:23:13 +01002571 'Do you want to submit latest Gerrit patchset and bypass hooks?\n'
2572 'Press Enter to continue, Ctrl+C to abort.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002573 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2574 elif not bypass_hooks:
2575 hook_results = self.RunHook(
2576 committing=True,
2577 may_prompt=not force,
2578 verbose=verbose,
2579 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2580 if not hook_results.should_continue():
2581 return 1
2582
2583 self.SubmitIssue(wait_for_merge=True)
2584 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002585 links = self._GetChangeCommit().get('web_links', [])
2586 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002587 if link.get('name') == 'gitiles' and link.get('url'):
agable32978d92016-11-01 12:55:02 -07002588 print('Landed as %s' % link.get('url'))
2589 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002590 return 0
2591
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002592 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2593 directory):
2594 assert not reject
2595 assert not nocommit
2596 assert not directory
2597 assert parsed_issue_arg.valid
2598
2599 self._changelist.issue = parsed_issue_arg.issue
2600
2601 if parsed_issue_arg.hostname:
2602 self._gerrit_host = parsed_issue_arg.hostname
2603 self._gerrit_server = 'https://%s' % self._gerrit_host
2604
tandriic2405f52016-10-10 08:13:15 -07002605 try:
2606 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002607 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002608 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002609
2610 if not parsed_issue_arg.patchset:
2611 # Use current revision by default.
2612 revision_info = detail['revisions'][detail['current_revision']]
2613 patchset = int(revision_info['_number'])
2614 else:
2615 patchset = parsed_issue_arg.patchset
2616 for revision_info in detail['revisions'].itervalues():
2617 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2618 break
2619 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002620 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002621 (parsed_issue_arg.patchset, self.GetIssue()))
2622
2623 fetch_info = revision_info['fetch']['http']
2624 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2625 RunGit(['cherry-pick', 'FETCH_HEAD'])
2626 self.SetIssue(self.GetIssue())
2627 self.SetPatchset(patchset)
Aaron Gablea45ee112016-11-22 15:14:38 -08002628 print('Committed patch for change %i patchset %i locally' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002629 (self.GetIssue(), self.GetPatchset()))
2630 return 0
2631
2632 @staticmethod
2633 def ParseIssueURL(parsed_url):
2634 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2635 return None
2636 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2637 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2638 # Short urls like https://domain/<issue_number> can be used, but don't allow
2639 # specifying the patchset (you'd 404), but we allow that here.
2640 if parsed_url.path == '/':
2641 part = parsed_url.fragment
2642 else:
2643 part = parsed_url.path
2644 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2645 if match:
2646 return _ParsedIssueNumberArgument(
2647 issue=int(match.group(2)),
2648 patchset=int(match.group(4)) if match.group(4) else None,
2649 hostname=parsed_url.netloc)
2650 return None
2651
tandrii16e0b4e2016-06-07 10:34:28 -07002652 def _GerritCommitMsgHookCheck(self, offer_removal):
2653 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2654 if not os.path.exists(hook):
2655 return
2656 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2657 # custom developer made one.
2658 data = gclient_utils.FileRead(hook)
2659 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2660 return
2661 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002662 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002663 'and may interfere with it in subtle ways.\n'
2664 'We recommend you remove the commit-msg hook.')
2665 if offer_removal:
2666 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2667 if reply.lower().startswith('y'):
2668 gclient_utils.rm_file_or_tree(hook)
2669 print('Gerrit commit-msg hook removed.')
2670 else:
2671 print('OK, will keep Gerrit commit-msg hook in place.')
2672
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002673 def CMDUploadChange(self, options, args, change):
2674 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002675 if options.squash and options.no_squash:
2676 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002677
2678 if not options.squash and not options.no_squash:
2679 # Load default for user, repo, squash=true, in this order.
2680 options.squash = settings.GetSquashGerritUploads()
2681 elif options.no_squash:
2682 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002683
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002684 # We assume the remote called "origin" is the one we want.
2685 # It is probably not worthwhile to support different workflows.
2686 gerrit_remote = 'origin'
2687
2688 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01002689 # Gerrit will not support pending prefix at all.
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002690 branch = GetTargetRef(remote, remote_branch, options.target_branch,
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01002691 pending_prefix_check=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002692
Aaron Gableb56ad332017-01-06 15:24:31 -08002693 # This may be None; default fallback value is determined in logic below.
2694 title = options.title
Aaron Gable856585d2017-01-23 15:32:00 -08002695 automatic_title = False
Aaron Gableb56ad332017-01-06 15:24:31 -08002696
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002697 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002698 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002699 if self.GetIssue():
2700 # Try to get the message from a previous upload.
2701 message = self.GetDescription()
2702 if not message:
2703 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002704 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002705 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002706 if not title:
2707 default_title = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
2708 title = ask_for_data(
2709 'Title for patchset [%s]: ' % default_title) or default_title
Aaron Gable856585d2017-01-23 15:32:00 -08002710 if title == default_title:
2711 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002712 change_id = self._GetChangeDetail()['change_id']
2713 while True:
2714 footer_change_ids = git_footers.get_footer_change_id(message)
2715 if footer_change_ids == [change_id]:
2716 break
2717 if not footer_change_ids:
2718 message = git_footers.add_footer_change_id(message, change_id)
Aaron Gablea45ee112016-11-22 15:14:38 -08002719 print('WARNING: appended missing Change-Id to change description')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002720 continue
2721 # There is already a valid footer but with different or several ids.
2722 # Doing this automatically is non-trivial as we don't want to lose
2723 # existing other footers, yet we want to append just 1 desired
2724 # Change-Id. Thus, just create a new footer, but let user verify the
2725 # new description.
2726 message = '%s\n\nChange-Id: %s' % (message, change_id)
2727 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002728 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002729 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002730 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002731 'Please, check the proposed correction to the description, '
2732 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2733 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2734 change_id))
2735 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2736 if not options.force:
2737 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002738 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002739 message = change_desc.description
2740 if not message:
2741 DieWithError("Description is empty. Aborting...")
2742 # Continue the while loop.
2743 # Sanity check of this code - we should end up with proper message
2744 # footer.
2745 assert [change_id] == git_footers.get_footer_change_id(message)
2746 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002747 else: # if not self.GetIssue()
2748 if options.message:
2749 message = options.message
2750 else:
2751 message = CreateDescriptionFromLog(args)
2752 if options.title:
2753 message = options.title + '\n\n' + message
2754 change_desc = ChangeDescription(message)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002755 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002756 change_desc.prompt(bug=options.bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002757 # On first upload, patchset title is always this string, while
2758 # --title flag gets converted to first line of message.
2759 title = 'Initial upload'
Aaron Gable856585d2017-01-23 15:32:00 -08002760 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002761 if not change_desc.description:
2762 DieWithError("Description is empty. Aborting...")
2763 message = change_desc.description
2764 change_ids = git_footers.get_footer_change_id(message)
2765 if len(change_ids) > 1:
2766 DieWithError('too many Change-Id footers, at most 1 allowed.')
2767 if not change_ids:
2768 # Generate the Change-Id automatically.
2769 message = git_footers.add_footer_change_id(
2770 message, GenerateGerritChangeId(message))
2771 change_desc.set_description(message)
2772 change_ids = git_footers.get_footer_change_id(message)
2773 assert len(change_ids) == 1
2774 change_id = change_ids[0]
2775
2776 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2777 if remote is '.':
2778 # If our upstream branch is local, we base our squashed commit on its
2779 # squashed version.
2780 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2781 # Check the squashed hash of the parent.
2782 parent = RunGit(['config',
2783 'branch.%s.gerritsquashhash' % upstream_branch_name],
2784 error_ok=True).strip()
2785 # Verify that the upstream branch has been uploaded too, otherwise
2786 # Gerrit will create additional CLs when uploading.
2787 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2788 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002789 DieWithError(
Aaron Gable48746652017-01-06 11:49:21 -08002790 '\nUpload upstream branch %s first.\n'
2791 'It is likely that this branch has been rebased since its last '
2792 'upload, so you just need to upload it again.\n'
2793 '(If you uploaded it with --no-squash, then branch dependencies '
2794 'are not supported, and you should reupload with --squash.)'
Christopher Lamf732cd52017-01-24 12:40:11 +11002795 % upstream_branch_name, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002796 else:
2797 parent = self.GetCommonAncestorWithUpstream()
2798
2799 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2800 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2801 '-m', message]).strip()
2802 else:
2803 change_desc = ChangeDescription(
2804 options.message or CreateDescriptionFromLog(args))
2805 if not change_desc.description:
2806 DieWithError("Description is empty. Aborting...")
2807
2808 if not git_footers.get_footer_change_id(change_desc.description):
2809 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002810 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2811 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002812 ref_to_push = 'HEAD'
2813 parent = '%s/%s' % (gerrit_remote, branch)
2814 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2815
2816 assert change_desc
2817 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2818 ref_to_push)]).splitlines()
2819 if len(commits) > 1:
2820 print('WARNING: This will upload %d commits. Run the following command '
2821 'to see which commits will be uploaded: ' % len(commits))
2822 print('git log %s..%s' % (parent, ref_to_push))
2823 print('You can also use `git squash-branch` to squash these into a '
2824 'single commit.')
2825 ask_for_data('About to upload; enter to confirm.')
2826
2827 if options.reviewers or options.tbr_owners:
2828 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2829 change)
2830
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002831 # Extra options that can be specified at push time. Doc:
2832 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2833 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002834 if change_desc.get_reviewers(tbr_only=True):
2835 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2836 refspec_opts.append('l=Code-Review+1')
2837
Aaron Gable9b713dd2016-12-14 16:04:21 -08002838 if title:
2839 if not re.match(r'^[\w ]+$', title):
2840 title = re.sub(r'[^\w ]', '', title)
Aaron Gable856585d2017-01-23 15:32:00 -08002841 if not automatic_title:
2842 print('WARNING: Patchset title may only contain alphanumeric chars '
2843 'and spaces. Cleaned up title:\n%s' % title)
2844 if not options.force:
2845 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002846 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2847 # reverse on its side.
Aaron Gable9b713dd2016-12-14 16:04:21 -08002848 refspec_opts.append('m=' + title.replace(' ', '_'))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002849
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002850 if options.send_mail:
2851 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002852 DieWithError('Must specify reviewers to send email.', change_desc)
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002853 refspec_opts.append('notify=ALL')
2854 else:
2855 refspec_opts.append('notify=NONE')
2856
tandrii99a72f22016-08-17 14:33:24 -07002857 reviewers = change_desc.get_reviewers()
2858 if reviewers:
2859 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002860
agablec6787972016-09-09 16:13:34 -07002861 if options.private:
2862 refspec_opts.append('draft')
2863
rmistry9eadede2016-09-19 11:22:43 -07002864 if options.topic:
2865 # Documentation on Gerrit topics is here:
2866 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2867 refspec_opts.append('topic=%s' % options.topic)
2868
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002869 refspec_suffix = ''
2870 if refspec_opts:
2871 refspec_suffix = '%' + ','.join(refspec_opts)
2872 assert ' ' not in refspec_suffix, (
2873 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002874 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002875
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002876 try:
2877 push_stdout = gclient_utils.CheckCallAndFilter(
2878 ['git', 'push', gerrit_remote, refspec],
2879 print_stdout=True,
2880 # Flush after every line: useful for seeing progress when running as
2881 # recipe.
2882 filter_fn=lambda _: sys.stdout.flush())
2883 except subprocess2.CalledProcessError:
2884 DieWithError('Failed to create a change. Please examine output above '
Christopher Lamf732cd52017-01-24 12:40:11 +11002885 'for the reason of the failure. ', change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002886
2887 if options.squash:
2888 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2889 change_numbers = [m.group(1)
2890 for m in map(regex.match, push_stdout.splitlines())
2891 if m]
2892 if len(change_numbers) != 1:
2893 DieWithError(
2894 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002895 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002896 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002897 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002898
2899 # Add cc's from the CC_LIST and --cc flag (if any).
2900 cc = self.GetCCList().split(',')
2901 if options.cc:
2902 cc.extend(options.cc)
2903 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002904 if change_desc.get_cced():
2905 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002906 if cc:
Aaron Gable5db82092016-11-17 10:52:08 -08002907 gerrit_util.AddReviewers(
Aaron Gable59f48512017-01-12 10:54:46 -08002908 self._GetGerritHost(), self.GetIssue(), cc,
2909 is_reviewer=False, notify=bool(options.send_mail))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002910 return 0
2911
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002912 def _AddChangeIdToCommitMessage(self, options, args):
2913 """Re-commits using the current message, assumes the commit hook is in
2914 place.
2915 """
2916 log_desc = options.message or CreateDescriptionFromLog(args)
2917 git_command = ['commit', '--amend', '-m', log_desc]
2918 RunGit(git_command)
2919 new_log_desc = CreateDescriptionFromLog(args)
2920 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002921 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002922 return new_log_desc
2923 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002924 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002925
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002926 def SetCQState(self, new_state):
2927 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002928 vote_map = {
2929 _CQState.NONE: 0,
2930 _CQState.DRY_RUN: 1,
2931 _CQState.COMMIT : 2,
2932 }
Andrii Shyshkalov828701b2016-12-09 10:46:47 +01002933 kwargs = {'labels': {'Commit-Queue': vote_map[new_state]}}
2934 if new_state == _CQState.DRY_RUN:
2935 # Don't spam everybody reviewer/owner.
2936 kwargs['notify'] = 'NONE'
2937 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(), **kwargs)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002938
tandriie113dfd2016-10-11 10:20:12 -07002939 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002940 try:
2941 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002942 except GerritChangeNotExists:
2943 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002944
2945 if data['status'] in ('ABANDONED', 'MERGED'):
2946 return 'CL %s is closed' % self.GetIssue()
2947
2948 def GetTryjobProperties(self, patchset=None):
2949 """Returns dictionary of properties to launch tryjob."""
2950 data = self._GetChangeDetail(['ALL_REVISIONS'])
2951 patchset = int(patchset or self.GetPatchset())
2952 assert patchset
2953 revision_data = None # Pylint wants it to be defined.
2954 for revision_data in data['revisions'].itervalues():
2955 if int(revision_data['_number']) == patchset:
2956 break
2957 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002958 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002959 (patchset, self.GetIssue()))
2960 return {
2961 'patch_issue': self.GetIssue(),
2962 'patch_set': patchset or self.GetPatchset(),
2963 'patch_project': data['project'],
2964 'patch_storage': 'gerrit',
2965 'patch_ref': revision_data['fetch']['http']['ref'],
2966 'patch_repository_url': revision_data['fetch']['http']['url'],
2967 'patch_gerrit_url': self.GetCodereviewServer(),
2968 }
tandriie113dfd2016-10-11 10:20:12 -07002969
tandriide281ae2016-10-12 06:02:30 -07002970 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002971 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002972
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002973
2974_CODEREVIEW_IMPLEMENTATIONS = {
2975 'rietveld': _RietveldChangelistImpl,
2976 'gerrit': _GerritChangelistImpl,
2977}
2978
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002979
iannuccie53c9352016-08-17 14:40:40 -07002980def _add_codereview_issue_select_options(parser, extra=""):
2981 _add_codereview_select_options(parser)
2982
2983 text = ('Operate on this issue number instead of the current branch\'s '
2984 'implicit issue.')
2985 if extra:
2986 text += ' '+extra
2987 parser.add_option('-i', '--issue', type=int, help=text)
2988
2989
2990def _process_codereview_issue_select_options(parser, options):
2991 _process_codereview_select_options(parser, options)
2992 if options.issue is not None and not options.forced_codereview:
2993 parser.error('--issue must be specified with either --rietveld or --gerrit')
2994
2995
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002996def _add_codereview_select_options(parser):
2997 """Appends --gerrit and --rietveld options to force specific codereview."""
2998 parser.codereview_group = optparse.OptionGroup(
2999 parser, 'EXPERIMENTAL! Codereview override options')
3000 parser.add_option_group(parser.codereview_group)
3001 parser.codereview_group.add_option(
3002 '--gerrit', action='store_true',
3003 help='Force the use of Gerrit for codereview')
3004 parser.codereview_group.add_option(
3005 '--rietveld', action='store_true',
3006 help='Force the use of Rietveld for codereview')
3007
3008
3009def _process_codereview_select_options(parser, options):
3010 if options.gerrit and options.rietveld:
3011 parser.error('Options --gerrit and --rietveld are mutually exclusive')
3012 options.forced_codereview = None
3013 if options.gerrit:
3014 options.forced_codereview = 'gerrit'
3015 elif options.rietveld:
3016 options.forced_codereview = 'rietveld'
3017
3018
tandriif9aefb72016-07-01 09:06:51 -07003019def _get_bug_line_values(default_project, bugs):
3020 """Given default_project and comma separated list of bugs, yields bug line
3021 values.
3022
3023 Each bug can be either:
3024 * a number, which is combined with default_project
3025 * string, which is left as is.
3026
3027 This function may produce more than one line, because bugdroid expects one
3028 project per line.
3029
3030 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3031 ['v8:123', 'chromium:789']
3032 """
3033 default_bugs = []
3034 others = []
3035 for bug in bugs.split(','):
3036 bug = bug.strip()
3037 if bug:
3038 try:
3039 default_bugs.append(int(bug))
3040 except ValueError:
3041 others.append(bug)
3042
3043 if default_bugs:
3044 default_bugs = ','.join(map(str, default_bugs))
3045 if default_project:
3046 yield '%s:%s' % (default_project, default_bugs)
3047 else:
3048 yield default_bugs
3049 for other in sorted(others):
3050 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3051 yield other
3052
3053
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003054class ChangeDescription(object):
3055 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003056 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003057 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00003058 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003059 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003060
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003061 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003062 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003063
agable@chromium.org42c20792013-09-12 17:34:49 +00003064 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003065 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003066 return '\n'.join(self._description_lines)
3067
3068 def set_description(self, desc):
3069 if isinstance(desc, basestring):
3070 lines = desc.splitlines()
3071 else:
3072 lines = [line.rstrip() for line in desc]
3073 while lines and not lines[0]:
3074 lines.pop(0)
3075 while lines and not lines[-1]:
3076 lines.pop(-1)
3077 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003078
piman@chromium.org336f9122014-09-04 02:16:55 +00003079 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003080 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003081 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00003082 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003083 return
agable@chromium.org42c20792013-09-12 17:34:49 +00003084 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003085
agable@chromium.org42c20792013-09-12 17:34:49 +00003086 # Get the set of R= and TBR= lines and remove them from the desciption.
3087 regexp = re.compile(self.R_LINE)
3088 matches = [regexp.match(line) for line in self._description_lines]
3089 new_desc = [l for i, l in enumerate(self._description_lines)
3090 if not matches[i]]
3091 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003092
agable@chromium.org42c20792013-09-12 17:34:49 +00003093 # Construct new unified R= and TBR= lines.
3094 r_names = []
3095 tbr_names = []
3096 for match in matches:
3097 if not match:
3098 continue
3099 people = cleanup_list([match.group(2).strip()])
3100 if match.group(1) == 'TBR':
3101 tbr_names.extend(people)
3102 else:
3103 r_names.extend(people)
3104 for name in r_names:
3105 if name not in reviewers:
3106 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003107 if add_owners_tbr:
3108 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003109 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003110 all_reviewers = set(tbr_names + reviewers)
3111 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3112 all_reviewers)
3113 tbr_names.extend(owners_db.reviewers_for(missing_files,
3114 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003115 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3116 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3117
3118 # Put the new lines in the description where the old first R= line was.
3119 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3120 if 0 <= line_loc < len(self._description_lines):
3121 if new_tbr_line:
3122 self._description_lines.insert(line_loc, new_tbr_line)
3123 if new_r_line:
3124 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003125 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003126 if new_r_line:
3127 self.append_footer(new_r_line)
3128 if new_tbr_line:
3129 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003130
tandriif9aefb72016-07-01 09:06:51 -07003131 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003132 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003133 self.set_description([
3134 '# Enter a description of the change.',
3135 '# This will be displayed on the codereview site.',
3136 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003137 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003138 '--------------------',
3139 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003140
agable@chromium.org42c20792013-09-12 17:34:49 +00003141 regexp = re.compile(self.BUG_LINE)
3142 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003143 prefix = settings.GetBugPrefix()
3144 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
3145 for value in values:
3146 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
3147 self.append_footer('BUG=%s' % value)
3148
agable@chromium.org42c20792013-09-12 17:34:49 +00003149 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003150 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003151 if not content:
3152 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003153 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003154
3155 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003156 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3157 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003158 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003159 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003160
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003161 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003162 """Adds a footer line to the description.
3163
3164 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3165 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3166 that Gerrit footers are always at the end.
3167 """
3168 parsed_footer_line = git_footers.parse_footer(line)
3169 if parsed_footer_line:
3170 # Line is a gerrit footer in the form: Footer-Key: any value.
3171 # Thus, must be appended observing Gerrit footer rules.
3172 self.set_description(
3173 git_footers.add_footer(self.description,
3174 key=parsed_footer_line[0],
3175 value=parsed_footer_line[1]))
3176 return
3177
3178 if not self._description_lines:
3179 self._description_lines.append(line)
3180 return
3181
3182 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3183 if gerrit_footers:
3184 # git_footers.split_footers ensures that there is an empty line before
3185 # actual (gerrit) footers, if any. We have to keep it that way.
3186 assert top_lines and top_lines[-1] == ''
3187 top_lines, separator = top_lines[:-1], top_lines[-1:]
3188 else:
3189 separator = [] # No need for separator if there are no gerrit_footers.
3190
3191 prev_line = top_lines[-1] if top_lines else ''
3192 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3193 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3194 top_lines.append('')
3195 top_lines.append(line)
3196 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003197
tandrii99a72f22016-08-17 14:33:24 -07003198 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003199 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003200 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003201 reviewers = [match.group(2).strip()
3202 for match in matches
3203 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003204 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003205
bradnelsond975b302016-10-23 12:20:23 -07003206 def get_cced(self):
3207 """Retrieves the list of reviewers."""
3208 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3209 cced = [match.group(2).strip() for match in matches if match]
3210 return cleanup_list(cced)
3211
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003212 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3213 """Updates this commit description given the parent.
3214
3215 This is essentially what Gnumbd used to do.
3216 Consult https://goo.gl/WMmpDe for more details.
3217 """
3218 assert parent_msg # No, orphan branch creation isn't supported.
3219 assert parent_hash
3220 assert dest_ref
3221 parent_footer_map = git_footers.parse_footers(parent_msg)
3222 # This will also happily parse svn-position, which GnumbD is no longer
3223 # supporting. While we'd generate correct footers, the verifier plugin
3224 # installed in Gerrit will block such commit (ie git push below will fail).
3225 parent_position = git_footers.get_position(parent_footer_map)
3226
3227 # Cherry-picks may have last line obscuring their prior footers,
3228 # from git_footers perspective. This is also what Gnumbd did.
3229 cp_line = None
3230 if (self._description_lines and
3231 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3232 cp_line = self._description_lines.pop()
3233
3234 top_lines, _, parsed_footers = git_footers.split_footers(self.description)
3235
3236 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3237 # user interference with actual footers we'd insert below.
3238 for i, (k, v) in enumerate(parsed_footers):
3239 if k.startswith('Cr-'):
3240 parsed_footers[i] = (k.replace('Cr-', 'Cr-Original-'), v)
3241
3242 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003243 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003244 if parent_position[0] == dest_ref:
3245 # Same branch as parent.
3246 number = int(parent_position[1]) + 1
3247 else:
3248 number = 1 # New branch, and extra lineage.
3249 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3250 int(parent_position[1])))
3251
3252 parsed_footers.append(('Cr-Commit-Position',
3253 '%s@{#%d}' % (dest_ref, number)))
3254 parsed_footers.extend(('Cr-Branched-From', v) for v in lineage)
3255
3256 self._description_lines = top_lines
3257 if cp_line:
3258 self._description_lines.append(cp_line)
3259 if self._description_lines[-1] != '':
3260 self._description_lines.append('') # Ensure footer separator.
3261 self._description_lines.extend('%s: %s' % kv for kv in parsed_footers)
3262
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003263
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003264def get_approving_reviewers(props):
3265 """Retrieves the reviewers that approved a CL from the issue properties with
3266 messages.
3267
3268 Note that the list may contain reviewers that are not committer, thus are not
3269 considered by the CQ.
3270 """
3271 return sorted(
3272 set(
3273 message['sender']
3274 for message in props['messages']
3275 if message['approval'] and message['sender'] in props['reviewers']
3276 )
3277 )
3278
3279
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003280def FindCodereviewSettingsFile(filename='codereview.settings'):
3281 """Finds the given file starting in the cwd and going up.
3282
3283 Only looks up to the top of the repository unless an
3284 'inherit-review-settings-ok' file exists in the root of the repository.
3285 """
3286 inherit_ok_file = 'inherit-review-settings-ok'
3287 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003288 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003289 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3290 root = '/'
3291 while True:
3292 if filename in os.listdir(cwd):
3293 if os.path.isfile(os.path.join(cwd, filename)):
3294 return open(os.path.join(cwd, filename))
3295 if cwd == root:
3296 break
3297 cwd = os.path.dirname(cwd)
3298
3299
3300def LoadCodereviewSettingsFromFile(fileobj):
3301 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003302 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003303
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003304 def SetProperty(name, setting, unset_error_ok=False):
3305 fullname = 'rietveld.' + name
3306 if setting in keyvals:
3307 RunGit(['config', fullname, keyvals[setting]])
3308 else:
3309 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3310
tandrii48df5812016-10-17 03:55:37 -07003311 if not keyvals.get('GERRIT_HOST', False):
3312 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003313 # Only server setting is required. Other settings can be absent.
3314 # In that case, we ignore errors raised during option deletion attempt.
3315 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003316 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003317 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3318 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003319 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003320 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3321 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003322 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003323 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003324 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3325 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003326
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003327 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003328 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003329
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003330 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003331 RunGit(['config', 'gerrit.squash-uploads',
3332 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003333
tandrii@chromium.org28253532016-04-14 13:46:56 +00003334 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003335 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003336 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3337
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003338 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3339 #should be of the form
3340 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3341 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
3342 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3343 keyvals['ORIGIN_URL_CONFIG']])
3344
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003345
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003346def urlretrieve(source, destination):
3347 """urllib is broken for SSL connections via a proxy therefore we
3348 can't use urllib.urlretrieve()."""
3349 with open(destination, 'w') as f:
3350 f.write(urllib2.urlopen(source).read())
3351
3352
ukai@chromium.org712d6102013-11-27 00:52:58 +00003353def hasSheBang(fname):
3354 """Checks fname is a #! script."""
3355 with open(fname) as f:
3356 return f.read(2).startswith('#!')
3357
3358
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003359# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3360def DownloadHooks(*args, **kwargs):
3361 pass
3362
3363
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003364def DownloadGerritHook(force):
3365 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003366
3367 Args:
3368 force: True to update hooks. False to install hooks if not present.
3369 """
3370 if not settings.GetIsGerrit():
3371 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003372 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003373 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3374 if not os.access(dst, os.X_OK):
3375 if os.path.exists(dst):
3376 if not force:
3377 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003378 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003379 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003380 if not hasSheBang(dst):
3381 DieWithError('Not a script: %s\n'
3382 'You need to download from\n%s\n'
3383 'into .git/hooks/commit-msg and '
3384 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003385 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3386 except Exception:
3387 if os.path.exists(dst):
3388 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003389 DieWithError('\nFailed to download hooks.\n'
3390 'You need to download from\n%s\n'
3391 'into .git/hooks/commit-msg and '
3392 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003393
3394
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003395
3396def GetRietveldCodereviewSettingsInteractively():
3397 """Prompt the user for settings."""
3398 server = settings.GetDefaultServerUrl(error_ok=True)
3399 prompt = 'Rietveld server (host[:port])'
3400 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3401 newserver = ask_for_data(prompt + ':')
3402 if not server and not newserver:
3403 newserver = DEFAULT_SERVER
3404 if newserver:
3405 newserver = gclient_utils.UpgradeToHttps(newserver)
3406 if newserver != server:
3407 RunGit(['config', 'rietveld.server', newserver])
3408
3409 def SetProperty(initial, caption, name, is_url):
3410 prompt = caption
3411 if initial:
3412 prompt += ' ("x" to clear) [%s]' % initial
3413 new_val = ask_for_data(prompt + ':')
3414 if new_val == 'x':
3415 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3416 elif new_val:
3417 if is_url:
3418 new_val = gclient_utils.UpgradeToHttps(new_val)
3419 if new_val != initial:
3420 RunGit(['config', 'rietveld.' + name, new_val])
3421
3422 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3423 SetProperty(settings.GetDefaultPrivateFlag(),
3424 'Private flag (rietveld only)', 'private', False)
3425 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3426 'tree-status-url', False)
3427 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3428 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3429 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3430 'run-post-upload-hook', False)
3431
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003432@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003433def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003434 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003435
tandrii5d0a0422016-09-14 06:24:35 -07003436 print('WARNING: git cl config works for Rietveld only')
3437 # TODO(tandrii): remove this once we switch to Gerrit.
3438 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003439 parser.add_option('--activate-update', action='store_true',
3440 help='activate auto-updating [rietveld] section in '
3441 '.git/config')
3442 parser.add_option('--deactivate-update', action='store_true',
3443 help='deactivate auto-updating [rietveld] section in '
3444 '.git/config')
3445 options, args = parser.parse_args(args)
3446
3447 if options.deactivate_update:
3448 RunGit(['config', 'rietveld.autoupdate', 'false'])
3449 return
3450
3451 if options.activate_update:
3452 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3453 return
3454
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003455 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003456 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003457 return 0
3458
3459 url = args[0]
3460 if not url.endswith('codereview.settings'):
3461 url = os.path.join(url, 'codereview.settings')
3462
3463 # Load code review settings and download hooks (if available).
3464 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3465 return 0
3466
3467
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003468def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003469 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003470 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3471 branch = ShortBranchName(branchref)
3472 _, args = parser.parse_args(args)
3473 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003474 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003475 return RunGit(['config', 'branch.%s.base-url' % branch],
3476 error_ok=False).strip()
3477 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003478 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003479 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3480 error_ok=False).strip()
3481
3482
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003483def color_for_status(status):
3484 """Maps a Changelist status to color, for CMDstatus and other tools."""
3485 return {
3486 'unsent': Fore.RED,
3487 'waiting': Fore.BLUE,
3488 'reply': Fore.YELLOW,
3489 'lgtm': Fore.GREEN,
3490 'commit': Fore.MAGENTA,
3491 'closed': Fore.CYAN,
3492 'error': Fore.WHITE,
3493 }.get(status, Fore.WHITE)
3494
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003495
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003496def get_cl_statuses(changes, fine_grained, max_processes=None):
3497 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003498
3499 If fine_grained is true, this will fetch CL statuses from the server.
3500 Otherwise, simply indicate if there's a matching url for the given branches.
3501
3502 If max_processes is specified, it is used as the maximum number of processes
3503 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3504 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003505
3506 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003507 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003508 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003509 upload.verbosity = 0
3510
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003511 if not changes:
3512 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003513
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003514 if not fine_grained:
3515 # Fast path which doesn't involve querying codereview servers.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003516 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003517 for cl in changes:
3518 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003519 return
3520
3521 # First, sort out authentication issues.
3522 logging.debug('ensuring credentials exist')
3523 for cl in changes:
3524 cl.EnsureAuthenticated(force=False, refresh=True)
3525
3526 def fetch(cl):
3527 try:
3528 return (cl, cl.GetStatus())
3529 except:
3530 # See http://crbug.com/629863.
3531 logging.exception('failed to fetch status for %s:', cl)
3532 raise
3533
3534 threads_count = len(changes)
3535 if max_processes:
3536 threads_count = max(1, min(threads_count, max_processes))
3537 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3538
3539 pool = ThreadPool(threads_count)
3540 fetched_cls = set()
3541 try:
3542 it = pool.imap_unordered(fetch, changes).__iter__()
3543 while True:
3544 try:
3545 cl, status = it.next(timeout=5)
3546 except multiprocessing.TimeoutError:
3547 break
3548 fetched_cls.add(cl)
3549 yield cl, status
3550 finally:
3551 pool.close()
3552
3553 # Add any branches that failed to fetch.
3554 for cl in set(changes) - fetched_cls:
3555 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003556
rmistry@google.com2dd99862015-06-22 12:22:18 +00003557
3558def upload_branch_deps(cl, args):
3559 """Uploads CLs of local branches that are dependents of the current branch.
3560
3561 If the local branch dependency tree looks like:
3562 test1 -> test2.1 -> test3.1
3563 -> test3.2
3564 -> test2.2 -> test3.3
3565
3566 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3567 run on the dependent branches in this order:
3568 test2.1, test3.1, test3.2, test2.2, test3.3
3569
3570 Note: This function does not rebase your local dependent branches. Use it when
3571 you make a change to the parent branch that will not conflict with its
3572 dependent branches, and you would like their dependencies updated in
3573 Rietveld.
3574 """
3575 if git_common.is_dirty_git_tree('upload-branch-deps'):
3576 return 1
3577
3578 root_branch = cl.GetBranch()
3579 if root_branch is None:
3580 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3581 'Get on a branch!')
3582 if not cl.GetIssue() or not cl.GetPatchset():
3583 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3584 'patchset dependencies without an uploaded CL.')
3585
3586 branches = RunGit(['for-each-ref',
3587 '--format=%(refname:short) %(upstream:short)',
3588 'refs/heads'])
3589 if not branches:
3590 print('No local branches found.')
3591 return 0
3592
3593 # Create a dictionary of all local branches to the branches that are dependent
3594 # on it.
3595 tracked_to_dependents = collections.defaultdict(list)
3596 for b in branches.splitlines():
3597 tokens = b.split()
3598 if len(tokens) == 2:
3599 branch_name, tracked = tokens
3600 tracked_to_dependents[tracked].append(branch_name)
3601
vapiera7fbd5a2016-06-16 09:17:49 -07003602 print()
3603 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003604 dependents = []
3605 def traverse_dependents_preorder(branch, padding=''):
3606 dependents_to_process = tracked_to_dependents.get(branch, [])
3607 padding += ' '
3608 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003609 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003610 dependents.append(dependent)
3611 traverse_dependents_preorder(dependent, padding)
3612 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003613 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003614
3615 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003616 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003617 return 0
3618
vapiera7fbd5a2016-06-16 09:17:49 -07003619 print('This command will checkout all dependent branches and run '
3620 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003621 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3622
andybons@chromium.org962f9462016-02-03 20:00:42 +00003623 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003624 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003625 args.extend(['-t', 'Updated patchset dependency'])
3626
rmistry@google.com2dd99862015-06-22 12:22:18 +00003627 # Record all dependents that failed to upload.
3628 failures = {}
3629 # Go through all dependents, checkout the branch and upload.
3630 try:
3631 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003632 print()
3633 print('--------------------------------------')
3634 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003635 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003636 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003637 try:
3638 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003639 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003640 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003641 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003642 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003643 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003644 finally:
3645 # Swap back to the original root branch.
3646 RunGit(['checkout', '-q', root_branch])
3647
vapiera7fbd5a2016-06-16 09:17:49 -07003648 print()
3649 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003650 for dependent_branch in dependents:
3651 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003652 print(' %s : %s' % (dependent_branch, upload_status))
3653 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003654
3655 return 0
3656
3657
kmarshall3bff56b2016-06-06 18:31:47 -07003658def CMDarchive(parser, args):
3659 """Archives and deletes branches associated with closed changelists."""
3660 parser.add_option(
3661 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003662 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003663 parser.add_option(
3664 '-f', '--force', action='store_true',
3665 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003666 parser.add_option(
3667 '-d', '--dry-run', action='store_true',
3668 help='Skip the branch tagging and removal steps.')
3669 parser.add_option(
3670 '-t', '--notags', action='store_true',
3671 help='Do not tag archived branches. '
3672 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003673
3674 auth.add_auth_options(parser)
3675 options, args = parser.parse_args(args)
3676 if args:
3677 parser.error('Unsupported args: %s' % ' '.join(args))
3678 auth_config = auth.extract_auth_config_from_options(options)
3679
3680 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3681 if not branches:
3682 return 0
3683
vapiera7fbd5a2016-06-16 09:17:49 -07003684 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003685 changes = [Changelist(branchref=b, auth_config=auth_config)
3686 for b in branches.splitlines()]
3687 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3688 statuses = get_cl_statuses(changes,
3689 fine_grained=True,
3690 max_processes=options.maxjobs)
3691 proposal = [(cl.GetBranch(),
3692 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3693 for cl, status in statuses
3694 if status == 'closed']
3695 proposal.sort()
3696
3697 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003698 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003699 return 0
3700
3701 current_branch = GetCurrentBranch()
3702
vapiera7fbd5a2016-06-16 09:17:49 -07003703 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003704 if options.notags:
3705 for next_item in proposal:
3706 print(' ' + next_item[0])
3707 else:
3708 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3709 for next_item in proposal:
3710 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003711
kmarshall9249e012016-08-23 12:02:16 -07003712 # Quit now on precondition failure or if instructed by the user, either
3713 # via an interactive prompt or by command line flags.
3714 if options.dry_run:
3715 print('\nNo changes were made (dry run).\n')
3716 return 0
3717 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003718 print('You are currently on a branch \'%s\' which is associated with a '
3719 'closed codereview issue, so archive cannot proceed. Please '
3720 'checkout another branch and run this command again.' %
3721 current_branch)
3722 return 1
kmarshall9249e012016-08-23 12:02:16 -07003723 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003724 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3725 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003726 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003727 return 1
3728
3729 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003730 if not options.notags:
3731 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003732 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003733
vapiera7fbd5a2016-06-16 09:17:49 -07003734 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003735
3736 return 0
3737
3738
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003739def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003740 """Show status of changelists.
3741
3742 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003743 - Red not sent for review or broken
3744 - Blue waiting for review
3745 - Yellow waiting for you to reply to review
3746 - Green LGTM'ed
3747 - Magenta in the commit queue
3748 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003749
3750 Also see 'git cl comments'.
3751 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003752 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003753 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003754 parser.add_option('-f', '--fast', action='store_true',
3755 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003756 parser.add_option(
3757 '-j', '--maxjobs', action='store', type=int,
3758 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003759
3760 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003761 _add_codereview_issue_select_options(
3762 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003763 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003764 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003765 if args:
3766 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003767 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003768
iannuccie53c9352016-08-17 14:40:40 -07003769 if options.issue is not None and not options.field:
3770 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003771
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003772 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003773 cl = Changelist(auth_config=auth_config, issue=options.issue,
3774 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003775 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003776 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003777 elif options.field == 'id':
3778 issueid = cl.GetIssue()
3779 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003780 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003781 elif options.field == 'patch':
3782 patchset = cl.GetPatchset()
3783 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003784 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003785 elif options.field == 'status':
3786 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003787 elif options.field == 'url':
3788 url = cl.GetIssueURL()
3789 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003790 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003791 return 0
3792
3793 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3794 if not branches:
3795 print('No local branch found.')
3796 return 0
3797
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003798 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003799 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003800 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003801 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003802 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003803 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003804 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003805
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003806 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003807 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3808 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3809 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003810 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003811 c, status = output.next()
3812 branch_statuses[c.GetBranch()] = status
3813 status = branch_statuses.pop(branch)
3814 url = cl.GetIssueURL()
3815 if url and (not status or status == 'error'):
3816 # The issue probably doesn't exist anymore.
3817 url += ' (broken)'
3818
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003819 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003820 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003821 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003822 color = ''
3823 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003824 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003825 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003826 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003827 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003828
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003829
3830 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07003831 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003832 print('Current branch: %s' % branch)
3833 for cl in changes:
3834 if cl.GetBranch() == branch:
3835 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003836 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003837 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003838 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003839 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003840 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003841 print('Issue description:')
3842 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003843 return 0
3844
3845
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003846def colorize_CMDstatus_doc():
3847 """To be called once in main() to add colors to git cl status help."""
3848 colors = [i for i in dir(Fore) if i[0].isupper()]
3849
3850 def colorize_line(line):
3851 for color in colors:
3852 if color in line.upper():
3853 # Extract whitespaces first and the leading '-'.
3854 indent = len(line) - len(line.lstrip(' ')) + 1
3855 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3856 return line
3857
3858 lines = CMDstatus.__doc__.splitlines()
3859 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3860
3861
phajdan.jre328cf92016-08-22 04:12:17 -07003862def write_json(path, contents):
3863 with open(path, 'w') as f:
3864 json.dump(contents, f)
3865
3866
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003867@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003868def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003869 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003870
3871 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003872 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003873 parser.add_option('-r', '--reverse', action='store_true',
3874 help='Lookup the branch(es) for the specified issues. If '
3875 'no issues are specified, all branches with mapped '
3876 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003877 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003878 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003879 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003880 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003881
dnj@chromium.org406c4402015-03-03 17:22:28 +00003882 if options.reverse:
3883 branches = RunGit(['for-each-ref', 'refs/heads',
3884 '--format=%(refname:short)']).splitlines()
3885
3886 # Reverse issue lookup.
3887 issue_branch_map = {}
3888 for branch in branches:
3889 cl = Changelist(branchref=branch)
3890 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3891 if not args:
3892 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003893 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003894 for issue in args:
3895 if not issue:
3896 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003897 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003898 print('Branch for issue number %s: %s' % (
3899 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003900 if options.json:
3901 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003902 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003903 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003904 if len(args) > 0:
3905 try:
3906 issue = int(args[0])
3907 except ValueError:
3908 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003909 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003910 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003911 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003912 if options.json:
3913 write_json(options.json, {
3914 'issue': cl.GetIssue(),
3915 'issue_url': cl.GetIssueURL(),
3916 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003917 return 0
3918
3919
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003920def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003921 """Shows or posts review comments for any changelist."""
3922 parser.add_option('-a', '--add-comment', dest='comment',
3923 help='comment to add to an issue')
3924 parser.add_option('-i', dest='issue',
3925 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003926 parser.add_option('-j', '--json-file',
3927 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003928 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003929 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003930 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003931
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003932 issue = None
3933 if options.issue:
3934 try:
3935 issue = int(options.issue)
3936 except ValueError:
3937 DieWithError('A review issue id is expected to be a number')
3938
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003939 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003940
3941 if options.comment:
3942 cl.AddComment(options.comment)
3943 return 0
3944
3945 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003946 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003947 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003948 summary.append({
3949 'date': message['date'],
3950 'lgtm': False,
3951 'message': message['text'],
3952 'not_lgtm': False,
3953 'sender': message['sender'],
3954 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003955 if message['disapproval']:
3956 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003957 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003958 elif message['approval']:
3959 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003960 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003961 elif message['sender'] == data['owner_email']:
3962 color = Fore.MAGENTA
3963 else:
3964 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003965 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003966 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003967 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003968 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003969 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003970 if options.json_file:
3971 with open(options.json_file, 'wb') as f:
3972 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003973 return 0
3974
3975
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003976@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003977def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003978 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003979 parser.add_option('-d', '--display', action='store_true',
3980 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003981 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003982 help='New description to set for this issue (- for stdin, '
3983 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003984 parser.add_option('-f', '--force', action='store_true',
3985 help='Delete any unpublished Gerrit edits for this issue '
3986 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003987
3988 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003989 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003990 options, args = parser.parse_args(args)
3991 _process_codereview_select_options(parser, options)
3992
3993 target_issue = None
3994 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003995 target_issue = ParseIssueNumberArgument(args[0])
3996 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003997 parser.print_help()
3998 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003999
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004000 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004001
martiniss6eda05f2016-06-30 10:18:35 -07004002 kwargs = {
4003 'auth_config': auth_config,
4004 'codereview': options.forced_codereview,
4005 }
4006 if target_issue:
4007 kwargs['issue'] = target_issue.issue
4008 if options.forced_codereview == 'rietveld':
4009 kwargs['rietveld_server'] = target_issue.hostname
4010
4011 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004012
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004013 if not cl.GetIssue():
4014 DieWithError('This branch has no associated changelist.')
4015 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004016
smut@google.com34fb6b12015-07-13 20:03:26 +00004017 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004018 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004019 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004020
4021 if options.new_description:
4022 text = options.new_description
4023 if text == '-':
4024 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004025 elif text == '+':
4026 base_branch = cl.GetCommonAncestorWithUpstream()
4027 change = cl.GetChange(base_branch, None, local_description=True)
4028 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004029
4030 description.set_description(text)
4031 else:
4032 description.prompt()
4033
wychen@chromium.org063e4e52015-04-03 06:51:44 +00004034 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004035 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004036 return 0
4037
4038
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004039def CreateDescriptionFromLog(args):
4040 """Pulls out the commit log to use as a base for the CL description."""
4041 log_args = []
4042 if len(args) == 1 and not args[0].endswith('.'):
4043 log_args = [args[0] + '..']
4044 elif len(args) == 1 and args[0].endswith('...'):
4045 log_args = [args[0][:-1]]
4046 elif len(args) == 2:
4047 log_args = [args[0] + '..' + args[1]]
4048 else:
4049 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004050 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004051
4052
thestig@chromium.org44202a22014-03-11 19:22:18 +00004053def CMDlint(parser, args):
4054 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004055 parser.add_option('--filter', action='append', metavar='-x,+y',
4056 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004057 auth.add_auth_options(parser)
4058 options, args = parser.parse_args(args)
4059 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004060
4061 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004062 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004063 try:
4064 import cpplint
4065 import cpplint_chromium
4066 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004067 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004068 return 1
4069
4070 # Change the current working directory before calling lint so that it
4071 # shows the correct base.
4072 previous_cwd = os.getcwd()
4073 os.chdir(settings.GetRoot())
4074 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004075 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004076 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4077 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004078 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004079 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004080 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004081
4082 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004083 command = args + files
4084 if options.filter:
4085 command = ['--filter=' + ','.join(options.filter)] + command
4086 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004087
4088 white_regex = re.compile(settings.GetLintRegex())
4089 black_regex = re.compile(settings.GetLintIgnoreRegex())
4090 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4091 for filename in filenames:
4092 if white_regex.match(filename):
4093 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004094 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004095 else:
4096 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4097 extra_check_functions)
4098 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004099 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004100 finally:
4101 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004102 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004103 if cpplint._cpplint_state.error_count != 0:
4104 return 1
4105 return 0
4106
4107
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004108def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004109 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004110 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004111 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004112 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004113 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004114 auth.add_auth_options(parser)
4115 options, args = parser.parse_args(args)
4116 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004117
sbc@chromium.org71437c02015-04-09 19:29:40 +00004118 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004119 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004120 return 1
4121
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004122 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004123 if args:
4124 base_branch = args[0]
4125 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004126 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004127 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004128
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004129 cl.RunHook(
4130 committing=not options.upload,
4131 may_prompt=False,
4132 verbose=options.verbose,
4133 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004134 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004135
4136
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004137def GenerateGerritChangeId(message):
4138 """Returns Ixxxxxx...xxx change id.
4139
4140 Works the same way as
4141 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4142 but can be called on demand on all platforms.
4143
4144 The basic idea is to generate git hash of a state of the tree, original commit
4145 message, author/committer info and timestamps.
4146 """
4147 lines = []
4148 tree_hash = RunGitSilent(['write-tree'])
4149 lines.append('tree %s' % tree_hash.strip())
4150 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4151 if code == 0:
4152 lines.append('parent %s' % parent.strip())
4153 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4154 lines.append('author %s' % author.strip())
4155 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4156 lines.append('committer %s' % committer.strip())
4157 lines.append('')
4158 # Note: Gerrit's commit-hook actually cleans message of some lines and
4159 # whitespace. This code is not doing this, but it clearly won't decrease
4160 # entropy.
4161 lines.append(message)
4162 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4163 stdin='\n'.join(lines))
4164 return 'I%s' % change_hash.strip()
4165
4166
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004167def GetTargetRef(remote, remote_branch, target_branch, pending_prefix_check,
4168 remote_url=None):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004169 """Computes the remote branch ref to use for the CL.
4170
4171 Args:
4172 remote (str): The git remote for the CL.
4173 remote_branch (str): The git remote branch for the CL.
4174 target_branch (str): The target branch specified by the user.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004175 pending_prefix_check (bool): If true, determines if pending_prefix should be
4176 used.
4177 remote_url (str): Only used for checking if pending_prefix should be used.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004178 """
4179 if not (remote and remote_branch):
4180 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004181
wittman@chromium.org455dc922015-01-26 20:15:50 +00004182 if target_branch:
4183 # Cannonicalize branch references to the equivalent local full symbolic
4184 # refs, which are then translated into the remote full symbolic refs
4185 # below.
4186 if '/' not in target_branch:
4187 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4188 else:
4189 prefix_replacements = (
4190 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4191 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4192 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4193 )
4194 match = None
4195 for regex, replacement in prefix_replacements:
4196 match = re.search(regex, target_branch)
4197 if match:
4198 remote_branch = target_branch.replace(match.group(0), replacement)
4199 break
4200 if not match:
4201 # This is a branch path but not one we recognize; use as-is.
4202 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004203 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4204 # Handle the refs that need to land in different refs.
4205 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004206
wittman@chromium.org455dc922015-01-26 20:15:50 +00004207 # Create the true path to the remote branch.
4208 # Does the following translation:
4209 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4210 # * refs/remotes/origin/master -> refs/heads/master
4211 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4212 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4213 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4214 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4215 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4216 'refs/heads/')
4217 elif remote_branch.startswith('refs/remotes/branch-heads'):
4218 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004219
4220 if pending_prefix_check:
4221 # If a pending prefix exists then replace refs/ with it.
4222 state = _GitNumbererState.load(remote_url, remote_branch)
4223 if state.pending_prefix:
4224 remote_branch = remote_branch.replace('refs/', state.pending_prefix)
wittman@chromium.org455dc922015-01-26 20:15:50 +00004225 return remote_branch
4226
4227
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004228def cleanup_list(l):
4229 """Fixes a list so that comma separated items are put as individual items.
4230
4231 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4232 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4233 """
4234 items = sum((i.split(',') for i in l), [])
4235 stripped_items = (i.strip() for i in items)
4236 return sorted(filter(None, stripped_items))
4237
4238
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004239@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004240def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004241 """Uploads the current changelist to codereview.
4242
4243 Can skip dependency patchset uploads for a branch by running:
4244 git config branch.branch_name.skip-deps-uploads True
4245 To unset run:
4246 git config --unset branch.branch_name.skip-deps-uploads
4247 Can also set the above globally by using the --global flag.
4248 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004249 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4250 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004251 parser.add_option('--bypass-watchlists', action='store_true',
4252 dest='bypass_watchlists',
4253 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004254 parser.add_option('-f', action='store_true', dest='force',
4255 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004256 parser.add_option('--message', '-m', dest='message',
4257 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004258 parser.add_option('-b', '--bug',
4259 help='pre-populate the bug number(s) for this issue. '
4260 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004261 parser.add_option('--message-file', dest='message_file',
4262 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004263 parser.add_option('--title', '-t', dest='title',
4264 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004265 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004266 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004267 help='reviewer email addresses')
4268 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004269 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004270 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004271 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004272 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004273 parser.add_option('--emulate_svn_auto_props',
4274 '--emulate-svn-auto-props',
4275 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004276 dest="emulate_svn_auto_props",
4277 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004278 parser.add_option('-c', '--use-commit-queue', action='store_true',
4279 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004280 parser.add_option('--private', action='store_true',
4281 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004282 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004283 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004284 metavar='TARGET',
4285 help='Apply CL to remote ref TARGET. ' +
4286 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004287 parser.add_option('--squash', action='store_true',
4288 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004289 parser.add_option('--no-squash', action='store_true',
4290 help='Don\'t squash multiple commits into one ' +
4291 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004292 parser.add_option('--topic', default=None,
4293 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004294 parser.add_option('--email', default=None,
4295 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004296 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4297 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004298 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4299 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004300 help='Send the patchset to do a CQ dry run right after '
4301 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004302 parser.add_option('--dependencies', action='store_true',
4303 help='Uploads CLs of all the local branches that depend on '
4304 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004305
rmistry@google.com2dd99862015-06-22 12:22:18 +00004306 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004307 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004308 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004309 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004310 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004311 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004312 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004313
sbc@chromium.org71437c02015-04-09 19:29:40 +00004314 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004315 return 1
4316
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004317 options.reviewers = cleanup_list(options.reviewers)
4318 options.cc = cleanup_list(options.cc)
4319
tandriib80458a2016-06-23 12:20:07 -07004320 if options.message_file:
4321 if options.message:
4322 parser.error('only one of --message and --message-file allowed.')
4323 options.message = gclient_utils.FileRead(options.message_file)
4324 options.message_file = None
4325
tandrii4d0545a2016-07-06 03:56:49 -07004326 if options.cq_dry_run and options.use_commit_queue:
4327 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4328
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004329 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4330 settings.GetIsGerrit()
4331
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004332 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004333 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004334
4335
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004336def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004337 print()
4338 print('Waiting for commit to be landed on %s...' % real_ref)
4339 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004340 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4341 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004342 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004343
4344 loop = 0
4345 while True:
4346 sys.stdout.write('fetching (%d)... \r' % loop)
4347 sys.stdout.flush()
4348 loop += 1
4349
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004350 if mirror:
4351 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004352 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4353 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4354 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4355 for commit in commits.splitlines():
4356 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004357 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004358 return commit
4359
4360 current_rev = to_rev
4361
4362
tandriibf429402016-09-14 07:09:12 -07004363def PushToGitPending(remote, pending_ref):
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004364 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4365
4366 Returns:
4367 (retcode of last operation, output log of last operation).
4368 """
4369 assert pending_ref.startswith('refs/'), pending_ref
4370 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4371 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4372 code = 0
4373 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004374 max_attempts = 3
4375 attempts_left = max_attempts
4376 while attempts_left:
4377 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004378 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004379 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004380
4381 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004382 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004383 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004384 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004385 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004386 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004387 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004388 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004389 continue
4390
4391 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004392 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004393 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004394 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004395 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004396 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4397 'the following files have merge conflicts:' % pending_ref)
4398 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4399 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004400 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004401 return code, out
4402
4403 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004404 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004405 code, out = RunGitWithCode(
4406 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4407 if code == 0:
4408 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004409 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004410 return code, out
4411
vapiera7fbd5a2016-06-16 09:17:49 -07004412 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004413 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004414 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004415 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004416 print('Fatal push error. Make sure your .netrc credentials and git '
4417 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004418 return code, out
4419
vapiera7fbd5a2016-06-16 09:17:49 -07004420 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004421 return code, out
4422
4423
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004424def IsFatalPushFailure(push_stdout):
4425 """True if retrying push won't help."""
4426 return '(prohibited by Gerrit)' in push_stdout
4427
4428
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004429@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004430def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004431 """DEPRECATED: Used to commit the current changelist via git-svn."""
4432 message = ('git-cl no longer supports committing to SVN repositories via '
4433 'git-svn. You probably want to use `git cl land` instead.')
4434 print(message)
4435 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004436
4437
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004438@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004439def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004440 """Commits the current changelist via git.
4441
4442 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4443 upstream and closes the issue automatically and atomically.
4444
4445 Otherwise (in case of Rietveld):
4446 Squashes branch into a single commit.
4447 Updates commit message with metadata (e.g. pointer to review).
4448 Pushes the code upstream.
4449 Updates review and closes.
4450 """
4451 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4452 help='bypass upload presubmit hook')
4453 parser.add_option('-m', dest='message',
4454 help="override review description")
4455 parser.add_option('-f', action='store_true', dest='force',
4456 help="force yes to questions (don't prompt)")
4457 parser.add_option('-c', dest='contributor',
4458 help="external contributor for patch (appended to " +
4459 "description and used as author for git). Should be " +
4460 "formatted as 'First Last <email@example.com>'")
4461 add_git_similarity(parser)
4462 auth.add_auth_options(parser)
4463 (options, args) = parser.parse_args(args)
4464 auth_config = auth.extract_auth_config_from_options(options)
4465
4466 cl = Changelist(auth_config=auth_config)
4467
4468 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4469 if cl.IsGerrit():
4470 if options.message:
4471 # This could be implemented, but it requires sending a new patch to
4472 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4473 # Besides, Gerrit has the ability to change the commit message on submit
4474 # automatically, thus there is no need to support this option (so far?).
4475 parser.error('-m MESSAGE option is not supported for Gerrit.')
4476 if options.contributor:
4477 parser.error(
4478 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4479 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4480 'the contributor\'s "name <email>". If you can\'t upload such a '
4481 'commit for review, contact your repository admin and request'
4482 '"Forge-Author" permission.')
4483 if not cl.GetIssue():
4484 DieWithError('You must upload the change first to Gerrit.\n'
4485 ' If you would rather have `git cl land` upload '
4486 'automatically for you, see http://crbug.com/642759')
4487 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4488 options.verbose)
4489
4490 current = cl.GetBranch()
4491 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4492 if remote == '.':
4493 print()
4494 print('Attempting to push branch %r into another local branch!' % current)
4495 print()
4496 print('Either reparent this branch on top of origin/master:')
4497 print(' git reparent-branch --root')
4498 print()
4499 print('OR run `git rebase-update` if you think the parent branch is ')
4500 print('already committed.')
4501 print()
4502 print(' Current parent: %r' % upstream_branch)
4503 return 1
4504
4505 if not args:
4506 # Default to merging against our best guess of the upstream branch.
4507 args = [cl.GetUpstreamBranch()]
4508
4509 if options.contributor:
4510 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
4511 print("Please provide contibutor as 'First Last <email@example.com>'")
4512 return 1
4513
4514 base_branch = args[0]
4515
4516 if git_common.is_dirty_git_tree('land'):
4517 return 1
4518
4519 # This rev-list syntax means "show all commits not in my branch that
4520 # are in base_branch".
4521 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4522 base_branch]).splitlines()
4523 if upstream_commits:
4524 print('Base branch "%s" has %d commits '
4525 'not in this branch.' % (base_branch, len(upstream_commits)))
4526 print('Run "git merge %s" before attempting to land.' % base_branch)
4527 return 1
4528
4529 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
4530 if not options.bypass_hooks:
4531 author = None
4532 if options.contributor:
4533 author = re.search(r'\<(.*)\>', options.contributor).group(1)
4534 hook_results = cl.RunHook(
4535 committing=True,
4536 may_prompt=not options.force,
4537 verbose=options.verbose,
4538 change=cl.GetChange(merge_base, author))
4539 if not hook_results.should_continue():
4540 return 1
4541
4542 # Check the tree status if the tree status URL is set.
4543 status = GetTreeStatus()
4544 if 'closed' == status:
4545 print('The tree is closed. Please wait for it to reopen. Use '
4546 '"git cl land --bypass-hooks" to commit on a closed tree.')
4547 return 1
4548 elif 'unknown' == status:
4549 print('Unable to determine tree status. Please verify manually and '
4550 'use "git cl land --bypass-hooks" to commit on a closed tree.')
4551 return 1
4552
4553 change_desc = ChangeDescription(options.message)
4554 if not change_desc.description and cl.GetIssue():
4555 change_desc = ChangeDescription(cl.GetDescription())
4556
4557 if not change_desc.description:
4558 if not cl.GetIssue() and options.bypass_hooks:
4559 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
4560 else:
4561 print('No description set.')
4562 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
4563 return 1
4564
4565 # Keep a separate copy for the commit message, because the commit message
4566 # contains the link to the Rietveld issue, while the Rietveld message contains
4567 # the commit viewvc url.
4568 if cl.GetIssue():
4569 change_desc.update_reviewers(cl.GetApprovingReviewers())
4570
4571 commit_desc = ChangeDescription(change_desc.description)
4572 if cl.GetIssue():
4573 # Xcode won't linkify this URL unless there is a non-whitespace character
4574 # after it. Add a period on a new line to circumvent this. Also add a space
4575 # before the period to make sure that Gitiles continues to correctly resolve
4576 # the URL.
4577 commit_desc.append_footer('Review-Url: %s .' % cl.GetIssueURL())
4578 if options.contributor:
4579 commit_desc.append_footer('Patch from %s.' % options.contributor)
4580
4581 print('Description:')
4582 print(commit_desc.description)
4583
4584 branches = [merge_base, cl.GetBranchRef()]
4585 if not options.force:
4586 print_stats(options.similarity, options.find_copies, branches)
4587
4588 # We want to squash all this branch's commits into one commit with the proper
4589 # description. We do this by doing a "reset --soft" to the base branch (which
4590 # keeps the working copy the same), then landing that.
4591 MERGE_BRANCH = 'git-cl-commit'
4592 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4593 # Delete the branches if they exist.
4594 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4595 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4596 result = RunGitWithCode(showref_cmd)
4597 if result[0] == 0:
4598 RunGit(['branch', '-D', branch])
4599
4600 # We might be in a directory that's present in this branch but not in the
4601 # trunk. Move up to the top of the tree so that git commands that expect a
4602 # valid CWD won't fail after we check out the merge branch.
4603 rel_base_path = settings.GetRelativeRoot()
4604 if rel_base_path:
4605 os.chdir(rel_base_path)
4606
4607 # Stuff our change into the merge branch.
4608 # We wrap in a try...finally block so if anything goes wrong,
4609 # we clean up the branches.
4610 retcode = -1
4611 pushed_to_pending = False
4612 pending_ref = None
4613 revision = None
4614 try:
4615 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
4616 RunGit(['reset', '--soft', merge_base])
4617 if options.contributor:
4618 RunGit(
4619 [
4620 'commit', '--author', options.contributor,
4621 '-m', commit_desc.description,
4622 ])
4623 else:
4624 RunGit(['commit', '-m', commit_desc.description])
4625
4626 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
4627 mirror = settings.GetGitMirror(remote)
4628 if mirror:
4629 pushurl = mirror.url
4630 git_numberer = _GitNumbererState.load(pushurl, branch)
4631 else:
4632 pushurl = remote # Usually, this is 'origin'.
4633 git_numberer = _GitNumbererState.load(
4634 RunGit(['config', 'remote.%s.url' % remote]).strip(), branch)
4635
4636 if git_numberer.should_add_git_number:
4637 # TODO(tandrii): run git fetch in a loop + autorebase when there there
4638 # is no pending ref to push to?
4639 logging.debug('Adding git number footers')
4640 parent_msg = RunGit(['show', '-s', '--format=%B', merge_base]).strip()
4641 commit_desc.update_with_git_number_footers(merge_base, parent_msg,
4642 branch)
4643 # Ensure timestamps are monotonically increasing.
4644 timestamp = max(1 + _get_committer_timestamp(merge_base),
4645 _get_committer_timestamp('HEAD'))
4646 _git_amend_head(commit_desc.description, timestamp)
4647 change_desc = ChangeDescription(commit_desc.description)
4648 # If gnumbd is sitll ON and we ultimately push to branch with
4649 # pending_prefix, gnumbd will modify footers we've just inserted with
4650 # 'Original-', which is annoying but still technically correct.
4651
4652 pending_prefix = git_numberer.pending_prefix
4653 if not pending_prefix or branch.startswith(pending_prefix):
4654 # If not using refs/pending/heads/* at all, or target ref is already set
4655 # to pending, then push to the target ref directly.
4656 # NB(tandrii): I think branch.startswith(pending_prefix) never happens
4657 # in practise. I really tried to create a new branch tracking
4658 # refs/pending/heads/master directly and git cl land failed long before
4659 # reaching this. Disagree? Comment on http://crbug.com/642493.
4660 if pending_prefix:
4661 print('\n\nYOU GOT A CHANCE TO WIN A FREE GIFT!\n\n'
4662 'Grab your .git/config, add instructions how to reproduce '
4663 'this, and post it to http://crbug.com/642493.\n'
4664 'The first reporter gets a free "Black Swan" book from '
4665 'tandrii@\n\n')
4666 retcode, output = RunGitWithCode(
4667 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
4668 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
4669 else:
4670 # Cherry-pick the change on top of pending ref and then push it.
4671 assert branch.startswith('refs/'), branch
4672 assert pending_prefix[-1] == '/', pending_prefix
4673 pending_ref = pending_prefix + branch[len('refs/'):]
4674 retcode, output = PushToGitPending(pushurl, pending_ref)
4675 pushed_to_pending = (retcode == 0)
4676
4677 if retcode == 0:
4678 revision = RunGit(['rev-parse', 'HEAD']).strip()
4679 logging.debug(output)
4680 except: # pylint: disable=bare-except
4681 if _IS_BEING_TESTED:
4682 logging.exception('this is likely your ACTUAL cause of test failure.\n'
4683 + '-' * 30 + '8<' + '-' * 30)
4684 logging.error('\n' + '-' * 30 + '8<' + '-' * 30 + '\n\n\n')
4685 raise
4686 finally:
4687 # And then swap back to the original branch and clean up.
4688 RunGit(['checkout', '-q', cl.GetBranch()])
4689 RunGit(['branch', '-D', MERGE_BRANCH])
4690
4691 if not revision:
4692 print('Failed to push. If this persists, please file a bug.')
4693 return 1
4694
4695 killed = False
4696 if pushed_to_pending:
4697 try:
4698 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4699 # We set pushed_to_pending to False, since it made it all the way to the
4700 # real ref.
4701 pushed_to_pending = False
4702 except KeyboardInterrupt:
4703 killed = True
4704
4705 if cl.GetIssue():
4706 to_pending = ' to pending queue' if pushed_to_pending else ''
4707 viewvc_url = settings.GetViewVCUrl()
4708 if not to_pending:
4709 if viewvc_url and revision:
4710 change_desc.append_footer(
4711 'Committed: %s%s' % (viewvc_url, revision))
4712 elif revision:
4713 change_desc.append_footer('Committed: %s' % (revision,))
4714 print('Closing issue '
4715 '(you may be prompted for your codereview password)...')
4716 cl.UpdateDescription(change_desc.description)
4717 cl.CloseIssue()
4718 props = cl.GetIssueProperties()
4719 patch_num = len(props['patchsets'])
4720 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
4721 patch_num, props['patchsets'][-1], to_pending, revision)
4722 if options.bypass_hooks:
4723 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4724 else:
4725 comment += ' (presubmit successful).'
4726 cl.RpcServer().add_comment(cl.GetIssue(), comment)
4727
4728 if pushed_to_pending:
4729 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
4730 print('The commit is in the pending queue (%s).' % pending_ref)
4731 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4732 'footer.' % branch)
4733
4734 if os.path.isfile(POSTUPSTREAM_HOOK):
4735 RunCommand([POSTUPSTREAM_HOOK, merge_base], error_ok=True)
4736
4737 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004738
4739
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004740@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004741def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004742 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004743 parser.add_option('-b', dest='newbranch',
4744 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004745 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004746 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004747 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4748 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004749 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004750 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004751 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004752 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004753 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004754 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004755
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004756
4757 group = optparse.OptionGroup(
4758 parser,
4759 'Options for continuing work on the current issue uploaded from a '
4760 'different clone (e.g. different machine). Must be used independently '
4761 'from the other options. No issue number should be specified, and the '
4762 'branch must have an issue number associated with it')
4763 group.add_option('--reapply', action='store_true', dest='reapply',
4764 help='Reset the branch and reapply the issue.\n'
4765 'CAUTION: This will undo any local changes in this '
4766 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004767
4768 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004769 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004770 parser.add_option_group(group)
4771
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004772 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004773 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004774 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004775 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004776 auth_config = auth.extract_auth_config_from_options(options)
4777
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004778
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004779 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004780 if options.newbranch:
4781 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004782 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004783 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004784
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004785 cl = Changelist(auth_config=auth_config,
4786 codereview=options.forced_codereview)
4787 if not cl.GetIssue():
4788 parser.error('current branch must have an associated issue')
4789
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004790 upstream = cl.GetUpstreamBranch()
4791 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004792 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004793
4794 RunGit(['reset', '--hard', upstream])
4795 if options.pull:
4796 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004797
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004798 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4799 options.directory)
4800
4801 if len(args) != 1 or not args[0]:
4802 parser.error('Must specify issue number or url')
4803
4804 # We don't want uncommitted changes mixed up with the patch.
4805 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004806 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004807
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004808 if options.newbranch:
4809 if options.force:
4810 RunGit(['branch', '-D', options.newbranch],
4811 stderr=subprocess2.PIPE, error_ok=True)
4812 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004813 elif not GetCurrentBranch():
4814 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004815
4816 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4817
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004818 if cl.IsGerrit():
4819 if options.reject:
4820 parser.error('--reject is not supported with Gerrit codereview.')
4821 if options.nocommit:
4822 parser.error('--nocommit is not supported with Gerrit codereview.')
4823 if options.directory:
4824 parser.error('--directory is not supported with Gerrit codereview.')
4825
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004826 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004827 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004828
4829
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004830def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004831 """Fetches the tree status and returns either 'open', 'closed',
4832 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004833 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004834 if url:
4835 status = urllib2.urlopen(url).read().lower()
4836 if status.find('closed') != -1 or status == '0':
4837 return 'closed'
4838 elif status.find('open') != -1 or status == '1':
4839 return 'open'
4840 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004841 return 'unset'
4842
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004843
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004844def GetTreeStatusReason():
4845 """Fetches the tree status from a json url and returns the message
4846 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004847 url = settings.GetTreeStatusUrl()
4848 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004849 connection = urllib2.urlopen(json_url)
4850 status = json.loads(connection.read())
4851 connection.close()
4852 return status['message']
4853
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004854
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004855def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004856 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004857 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004858 status = GetTreeStatus()
4859 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004860 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004861 return 2
4862
vapiera7fbd5a2016-06-16 09:17:49 -07004863 print('The tree is %s' % status)
4864 print()
4865 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004866 if status != 'open':
4867 return 1
4868 return 0
4869
4870
maruel@chromium.org15192402012-09-06 12:38:29 +00004871def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004872 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004873 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004874 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004875 '-b', '--bot', action='append',
4876 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4877 'times to specify multiple builders. ex: '
4878 '"-b win_rel -b win_layout". See '
4879 'the try server waterfall for the builders name and the tests '
4880 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004881 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004882 '-B', '--bucket', default='',
4883 help=('Buildbucket bucket to send the try requests.'))
4884 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004885 '-m', '--master', default='',
4886 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004887 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004888 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004889 help='Revision to use for the try job; default: the revision will '
4890 'be determined by the try recipe that builder runs, which usually '
4891 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004892 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004893 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004894 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004895 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004896 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004897 '--project',
4898 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004899 'in recipe to determine to which repository or directory to '
4900 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004901 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004902 '-p', '--property', dest='properties', action='append', default=[],
4903 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004904 'key2=value2 etc. The value will be treated as '
4905 'json if decodable, or as string otherwise. '
4906 'NOTE: using this may make your try job not usable for CQ, '
4907 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004908 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004909 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4910 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004911 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004912 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004913 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004914 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004915
machenbach@chromium.org45453142015-09-15 08:45:22 +00004916 # Make sure that all properties are prop=value pairs.
4917 bad_params = [x for x in options.properties if '=' not in x]
4918 if bad_params:
4919 parser.error('Got properties with missing "=": %s' % bad_params)
4920
maruel@chromium.org15192402012-09-06 12:38:29 +00004921 if args:
4922 parser.error('Unknown arguments: %s' % args)
4923
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004924 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004925 if not cl.GetIssue():
4926 parser.error('Need to upload first')
4927
tandriie113dfd2016-10-11 10:20:12 -07004928 error_message = cl.CannotTriggerTryJobReason()
4929 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004930 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004931
borenet6c0efe62016-10-19 08:13:29 -07004932 if options.bucket and options.master:
4933 parser.error('Only one of --bucket and --master may be used.')
4934
qyearsley1fdfcb62016-10-24 13:22:03 -07004935 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004936
qyearsleydd49f942016-10-28 11:57:22 -07004937 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4938 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004939 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004940 if options.verbose:
4941 print('git cl try with no bots now defaults to CQ Dry Run.')
4942 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004943
borenet6c0efe62016-10-19 08:13:29 -07004944 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004945 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004946 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004947 'of bot requires an initial job from a parent (usually a builder). '
4948 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004949 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004950 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004951
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004952 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004953 # TODO(tandrii): Checking local patchset against remote patchset is only
4954 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4955 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07004956 print('Warning: Codereview server has newer patchsets (%s) than most '
4957 'recent upload from local checkout (%s). Did a previous upload '
4958 'fail?\n'
4959 'By default, git cl try uses the latest patchset from '
4960 'codereview, continuing to use patchset %s.\n' %
4961 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07004962
tandrii568043b2016-10-11 07:49:18 -07004963 try:
borenet6c0efe62016-10-19 08:13:29 -07004964 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
4965 patchset)
tandrii568043b2016-10-11 07:49:18 -07004966 except BuildbucketResponseException as ex:
4967 print('ERROR: %s' % ex)
4968 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004969 return 0
4970
4971
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004972def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004973 """Prints info about try jobs associated with current CL."""
4974 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004975 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004976 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004977 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004978 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004979 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004980 '--color', action='store_true', default=setup_color.IS_TTY,
4981 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004982 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004983 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4984 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004985 group.add_option(
4986 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004987 parser.add_option_group(group)
4988 auth.add_auth_options(parser)
4989 options, args = parser.parse_args(args)
4990 if args:
4991 parser.error('Unrecognized args: %s' % ' '.join(args))
4992
4993 auth_config = auth.extract_auth_config_from_options(options)
4994 cl = Changelist(auth_config=auth_config)
4995 if not cl.GetIssue():
4996 parser.error('Need to upload first')
4997
tandrii221ab252016-10-06 08:12:04 -07004998 patchset = options.patchset
4999 if not patchset:
5000 patchset = cl.GetMostRecentPatchset()
5001 if not patchset:
5002 parser.error('Codereview doesn\'t know about issue %s. '
5003 'No access to issue or wrong issue number?\n'
5004 'Either upload first, or pass --patchset explicitely' %
5005 cl.GetIssue())
5006
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005007 # TODO(tandrii): Checking local patchset against remote patchset is only
5008 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5009 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07005010 print('Warning: Codereview server has newer patchsets (%s) than most '
5011 'recent upload from local checkout (%s). Did a previous upload '
5012 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07005013 'By default, git cl try-results uses the latest patchset from '
5014 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07005015 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005016 try:
tandrii221ab252016-10-06 08:12:04 -07005017 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005018 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005019 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005020 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005021 if options.json:
5022 write_try_results_json(options.json, jobs)
5023 else:
5024 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005025 return 0
5026
5027
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005028@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005029def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005030 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005031 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005032 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005033 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005034
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005035 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005036 if args:
5037 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005038 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005039 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005040 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005041 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005042
5043 # Clear configured merge-base, if there is one.
5044 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005045 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005046 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005047 return 0
5048
5049
thestig@chromium.org00858c82013-12-02 23:08:03 +00005050def CMDweb(parser, args):
5051 """Opens the current CL in the web browser."""
5052 _, args = parser.parse_args(args)
5053 if args:
5054 parser.error('Unrecognized args: %s' % ' '.join(args))
5055
5056 issue_url = Changelist().GetIssueURL()
5057 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005058 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005059 return 1
5060
5061 webbrowser.open(issue_url)
5062 return 0
5063
5064
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005065def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005066 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005067 parser.add_option('-d', '--dry-run', action='store_true',
5068 help='trigger in dry run mode')
5069 parser.add_option('-c', '--clear', action='store_true',
5070 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005071 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005072 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005073 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005074 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005075 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005076 if args:
5077 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005078 if options.dry_run and options.clear:
5079 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5080
iannuccie53c9352016-08-17 14:40:40 -07005081 cl = Changelist(auth_config=auth_config, issue=options.issue,
5082 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005083 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005084 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005085 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07005086 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005087 state = _CQState.DRY_RUN
5088 else:
5089 state = _CQState.COMMIT
5090 if not cl.GetIssue():
5091 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005092 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005093 return 0
5094
5095
groby@chromium.org411034a2013-02-26 15:12:01 +00005096def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005097 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005098 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005099 auth.add_auth_options(parser)
5100 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005101 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005102 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005103 if args:
5104 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005105 cl = Changelist(auth_config=auth_config, issue=options.issue,
5106 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005107 # Ensure there actually is an issue to close.
5108 cl.GetDescription()
5109 cl.CloseIssue()
5110 return 0
5111
5112
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005113def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005114 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005115 parser.add_option(
5116 '--stat',
5117 action='store_true',
5118 dest='stat',
5119 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005120 auth.add_auth_options(parser)
5121 options, args = parser.parse_args(args)
5122 auth_config = auth.extract_auth_config_from_options(options)
5123 if args:
5124 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005125
5126 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005127 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005128 # Staged changes would be committed along with the patch from last
5129 # upload, hence counted toward the "last upload" side in the final
5130 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005131 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005132 return 1
5133
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005134 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005135 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005136 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005137 if not issue:
5138 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005139 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005140 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005141
5142 # Create a new branch based on the merge-base
5143 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005144 # Clear cached branch in cl object, to avoid overwriting original CL branch
5145 # properties.
5146 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005147 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005148 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005149 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005150 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005151 return rtn
5152
wychen@chromium.org06928532015-02-03 02:11:29 +00005153 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005154 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005155 cmd = ['git', 'diff']
5156 if options.stat:
5157 cmd.append('--stat')
5158 cmd.extend([TMP_BRANCH, branch, '--'])
5159 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005160 finally:
5161 RunGit(['checkout', '-q', branch])
5162 RunGit(['branch', '-D', TMP_BRANCH])
5163
5164 return 0
5165
5166
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005167def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005168 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005169 parser.add_option(
5170 '--no-color',
5171 action='store_true',
5172 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005173 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005174 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005175 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005176
5177 author = RunGit(['config', 'user.email']).strip() or None
5178
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005179 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005180
5181 if args:
5182 if len(args) > 1:
5183 parser.error('Unknown args')
5184 base_branch = args[0]
5185 else:
5186 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005187 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005188
5189 change = cl.GetChange(base_branch, None)
5190 return owners_finder.OwnersFinder(
5191 [f.LocalPath() for f in
5192 cl.GetChange(base_branch, None).AffectedFiles()],
5193 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005194 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005195 disable_color=options.no_color).run()
5196
5197
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005198def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005199 """Generates a diff command."""
5200 # Generate diff for the current branch's changes.
5201 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
5202 upstream_commit, '--' ]
5203
5204 if args:
5205 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005206 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005207 diff_cmd.append(arg)
5208 else:
5209 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005210
5211 return diff_cmd
5212
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005213def MatchingFileType(file_name, extensions):
5214 """Returns true if the file name ends with one of the given extensions."""
5215 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005216
enne@chromium.org555cfe42014-01-29 18:21:39 +00005217@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005218def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005219 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005220 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005221 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005222 parser.add_option('--full', action='store_true',
5223 help='Reformat the full content of all touched files')
5224 parser.add_option('--dry-run', action='store_true',
5225 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005226 parser.add_option('--python', action='store_true',
5227 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005228 parser.add_option('--js', action='store_true',
5229 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005230 parser.add_option('--diff', action='store_true',
5231 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005232 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005233
Daniel Chengc55eecf2016-12-30 03:11:02 -08005234 # Normalize any remaining args against the current path, so paths relative to
5235 # the current directory are still resolved as expected.
5236 args = [os.path.join(os.getcwd(), arg) for arg in args]
5237
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005238 # git diff generates paths against the root of the repository. Change
5239 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005240 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005241 if rel_base_path:
5242 os.chdir(rel_base_path)
5243
digit@chromium.org29e47272013-05-17 17:01:46 +00005244 # Grab the merge-base commit, i.e. the upstream commit of the current
5245 # branch when it was created or the last time it was rebased. This is
5246 # to cover the case where the user may have called "git fetch origin",
5247 # moving the origin branch to a newer commit, but hasn't rebased yet.
5248 upstream_commit = None
5249 cl = Changelist()
5250 upstream_branch = cl.GetUpstreamBranch()
5251 if upstream_branch:
5252 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5253 upstream_commit = upstream_commit.strip()
5254
5255 if not upstream_commit:
5256 DieWithError('Could not find base commit for this branch. '
5257 'Are you in detached state?')
5258
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005259 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5260 diff_output = RunGit(changed_files_cmd)
5261 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005262 # Filter out files deleted by this CL
5263 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005264
Christopher Lamc5ba6922017-01-24 11:19:14 +11005265 if opts.js:
5266 CLANG_EXTS.append('.js')
5267
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005268 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5269 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5270 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005271 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005272
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005273 top_dir = os.path.normpath(
5274 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5275
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005276 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5277 # formatted. This is used to block during the presubmit.
5278 return_value = 0
5279
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005280 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005281 # Locate the clang-format binary in the checkout
5282 try:
5283 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005284 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005285 DieWithError(e)
5286
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005287 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005288 cmd = [clang_format_tool]
5289 if not opts.dry_run and not opts.diff:
5290 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005291 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005292 if opts.diff:
5293 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005294 else:
5295 env = os.environ.copy()
5296 env['PATH'] = str(os.path.dirname(clang_format_tool))
5297 try:
5298 script = clang_format.FindClangFormatScriptInChromiumTree(
5299 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005300 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005301 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005302
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005303 cmd = [sys.executable, script, '-p0']
5304 if not opts.dry_run and not opts.diff:
5305 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005306
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005307 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5308 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005309
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005310 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5311 if opts.diff:
5312 sys.stdout.write(stdout)
5313 if opts.dry_run and len(stdout) > 0:
5314 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005315
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005316 # Similar code to above, but using yapf on .py files rather than clang-format
5317 # on C/C++ files
5318 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005319 yapf_tool = gclient_utils.FindExecutable('yapf')
5320 if yapf_tool is None:
5321 DieWithError('yapf not found in PATH')
5322
5323 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005324 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005325 cmd = [yapf_tool]
5326 if not opts.dry_run and not opts.diff:
5327 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005328 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005329 if opts.diff:
5330 sys.stdout.write(stdout)
5331 else:
5332 # TODO(sbc): yapf --lines mode still has some issues.
5333 # https://github.com/google/yapf/issues/154
5334 DieWithError('--python currently only works with --full')
5335
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005336 # Dart's formatter does not have the nice property of only operating on
5337 # modified chunks, so hard code full.
5338 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005339 try:
5340 command = [dart_format.FindDartFmtToolInChromiumTree()]
5341 if not opts.dry_run and not opts.diff:
5342 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005343 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005344
ppi@chromium.org6593d932016-03-03 15:41:15 +00005345 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005346 if opts.dry_run and stdout:
5347 return_value = 2
5348 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005349 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5350 'found in this checkout. Files in other languages are still '
5351 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005352
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005353 # Format GN build files. Always run on full build files for canonical form.
5354 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005355 cmd = ['gn', 'format' ]
5356 if opts.dry_run or opts.diff:
5357 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005358 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005359 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5360 shell=sys.platform == 'win32',
5361 cwd=top_dir)
5362 if opts.dry_run and gn_ret == 2:
5363 return_value = 2 # Not formatted.
5364 elif opts.diff and gn_ret == 2:
5365 # TODO this should compute and print the actual diff.
5366 print("This change has GN build file diff for " + gn_diff_file)
5367 elif gn_ret != 0:
5368 # For non-dry run cases (and non-2 return values for dry-run), a
5369 # nonzero error code indicates a failure, probably because the file
5370 # doesn't parse.
5371 DieWithError("gn format failed on " + gn_diff_file +
5372 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005373
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005374 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005375
5376
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005377@subcommand.usage('<codereview url or issue id>')
5378def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005379 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005380 _, args = parser.parse_args(args)
5381
5382 if len(args) != 1:
5383 parser.print_help()
5384 return 1
5385
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005386 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005387 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005388 parser.print_help()
5389 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005390 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005391
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005392 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005393 output = RunGit(['config', '--local', '--get-regexp',
5394 r'branch\..*\.%s' % issueprefix],
5395 error_ok=True)
5396 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005397 if issue == target_issue:
5398 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005399
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005400 branches = []
5401 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005402 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005403 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005404 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005405 return 1
5406 if len(branches) == 1:
5407 RunGit(['checkout', branches[0]])
5408 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005409 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005410 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005411 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005412 which = raw_input('Choose by index: ')
5413 try:
5414 RunGit(['checkout', branches[int(which)]])
5415 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005416 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005417 return 1
5418
5419 return 0
5420
5421
maruel@chromium.org29404b52014-09-08 22:58:00 +00005422def CMDlol(parser, args):
5423 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005424 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005425 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5426 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5427 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005428 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005429 return 0
5430
5431
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005432class OptionParser(optparse.OptionParser):
5433 """Creates the option parse and add --verbose support."""
5434 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005435 optparse.OptionParser.__init__(
5436 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005437 self.add_option(
5438 '-v', '--verbose', action='count', default=0,
5439 help='Use 2 times for more debugging info')
5440
5441 def parse_args(self, args=None, values=None):
5442 options, args = optparse.OptionParser.parse_args(self, args, values)
5443 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005444 logging.basicConfig(
5445 level=levels[min(options.verbose, len(levels) - 1)],
5446 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5447 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005448 return options, args
5449
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005450
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005451def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005452 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005453 print('\nYour python version %s is unsupported, please upgrade.\n' %
5454 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005455 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005456
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005457 # Reload settings.
5458 global settings
5459 settings = Settings()
5460
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005461 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005462 dispatcher = subcommand.CommandDispatcher(__name__)
5463 try:
5464 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005465 except auth.AuthenticationError as e:
5466 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005467 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005468 if e.code != 500:
5469 raise
5470 DieWithError(
5471 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5472 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005473 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005474
5475
5476if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005477 # These affect sys.stdout so do it outside of main() to simplify mocks in
5478 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005479 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005480 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005481 try:
5482 sys.exit(main(sys.argv[1:]))
5483 except KeyboardInterrupt:
5484 sys.stderr.write('interrupted\n')
5485 sys.exit(1)