blob: de846dbacf04610df1170b3f6263d660fbd5bfe8 [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):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002465 data = self._GetChangeDetail(['CURRENT_REVISION'])
2466 current_rev = data['current_revision']
2467 url = data['revisions'][current_rev]['fetch']['http']['url']
2468 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002469
dsansomee2d6fd92016-09-08 00:10:47 -07002470 def UpdateDescriptionRemote(self, description, force=False):
2471 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2472 if not force:
2473 ask_for_data(
2474 'The description cannot be modified while the issue has a pending '
2475 'unpublished edit. Either publish the edit in the Gerrit web UI '
2476 'or delete it.\n\n'
2477 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2478
2479 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2480 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002481 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002482 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002483
2484 def CloseIssue(self):
2485 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2486
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002487 def GetApprovingReviewers(self):
2488 """Returns a list of reviewers approving the change.
2489
2490 Note: not necessarily committers.
2491 """
2492 raise NotImplementedError()
2493
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002494 def SubmitIssue(self, wait_for_merge=True):
2495 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2496 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002497
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002498 def _GetChangeDetail(self, options=None, issue=None,
2499 no_cache=False):
2500 """Returns details of the issue by querying Gerrit and caching results.
2501
2502 If fresh data is needed, set no_cache=True which will clear cache and
2503 thus new data will be fetched from Gerrit.
2504 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002505 options = options or []
2506 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002507 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002508
2509 # Normalize issue and options for consistent keys in cache.
2510 issue = str(issue)
2511 options = [o.upper() for o in options]
2512
2513 # Check in cache first unless no_cache is True.
2514 if no_cache:
2515 self._detail_cache.pop(issue, None)
2516 else:
2517 options_set = frozenset(options)
2518 for cached_options_set, data in self._detail_cache.get(issue, []):
2519 # Assumption: data fetched before with extra options is suitable
2520 # for return for a smaller set of options.
2521 # For example, if we cached data for
2522 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2523 # and request is for options=[CURRENT_REVISION],
2524 # THEN we can return prior cached data.
2525 if options_set.issubset(cached_options_set):
2526 return data
2527
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002528 try:
2529 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2530 options, ignore_404=False)
2531 except gerrit_util.GerritError as e:
2532 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002533 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002534 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002535
2536 self._detail_cache.setdefault(issue, []).append((frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002537 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002538
agable32978d92016-11-01 12:55:02 -07002539 def _GetChangeCommit(self, issue=None):
2540 issue = issue or self.GetIssue()
2541 assert issue, 'issue is required to query Gerrit'
2542 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2543 if not data:
Aaron Gablea45ee112016-11-22 15:14:38 -08002544 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
agable32978d92016-11-01 12:55:02 -07002545 return data
2546
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002547 def CMDLand(self, force, bypass_hooks, verbose):
2548 if git_common.is_dirty_git_tree('land'):
2549 return 1
tandriid60367b2016-06-22 05:25:12 -07002550 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2551 if u'Commit-Queue' in detail.get('labels', {}):
2552 if not force:
2553 ask_for_data('\nIt seems this repository has a Commit Queue, '
2554 'which can test and land changes for you. '
2555 'Are you sure you wish to bypass it?\n'
2556 'Press Enter to continue, Ctrl+C to abort.')
2557
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002558 differs = True
tandriic4344b52016-08-29 06:04:54 -07002559 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002560 # Note: git diff outputs nothing if there is no diff.
2561 if not last_upload or RunGit(['diff', last_upload]).strip():
2562 print('WARNING: some changes from local branch haven\'t been uploaded')
2563 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002564 if detail['current_revision'] == last_upload:
2565 differs = False
2566 else:
2567 print('WARNING: local branch contents differ from latest uploaded '
2568 'patchset')
2569 if differs:
2570 if not force:
2571 ask_for_data(
Andrii Shyshkalov3aac7752016-11-16 15:23:13 +01002572 'Do you want to submit latest Gerrit patchset and bypass hooks?\n'
2573 'Press Enter to continue, Ctrl+C to abort.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002574 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2575 elif not bypass_hooks:
2576 hook_results = self.RunHook(
2577 committing=True,
2578 may_prompt=not force,
2579 verbose=verbose,
2580 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2581 if not hook_results.should_continue():
2582 return 1
2583
2584 self.SubmitIssue(wait_for_merge=True)
2585 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002586 links = self._GetChangeCommit().get('web_links', [])
2587 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002588 if link.get('name') == 'gitiles' and link.get('url'):
agable32978d92016-11-01 12:55:02 -07002589 print('Landed as %s' % link.get('url'))
2590 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002591 return 0
2592
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002593 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2594 directory):
2595 assert not reject
2596 assert not nocommit
2597 assert not directory
2598 assert parsed_issue_arg.valid
2599
2600 self._changelist.issue = parsed_issue_arg.issue
2601
2602 if parsed_issue_arg.hostname:
2603 self._gerrit_host = parsed_issue_arg.hostname
2604 self._gerrit_server = 'https://%s' % self._gerrit_host
2605
tandriic2405f52016-10-10 08:13:15 -07002606 try:
2607 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002608 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002609 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002610
2611 if not parsed_issue_arg.patchset:
2612 # Use current revision by default.
2613 revision_info = detail['revisions'][detail['current_revision']]
2614 patchset = int(revision_info['_number'])
2615 else:
2616 patchset = parsed_issue_arg.patchset
2617 for revision_info in detail['revisions'].itervalues():
2618 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2619 break
2620 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002621 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002622 (parsed_issue_arg.patchset, self.GetIssue()))
2623
2624 fetch_info = revision_info['fetch']['http']
2625 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2626 RunGit(['cherry-pick', 'FETCH_HEAD'])
2627 self.SetIssue(self.GetIssue())
2628 self.SetPatchset(patchset)
Aaron Gablea45ee112016-11-22 15:14:38 -08002629 print('Committed patch for change %i patchset %i locally' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002630 (self.GetIssue(), self.GetPatchset()))
2631 return 0
2632
2633 @staticmethod
2634 def ParseIssueURL(parsed_url):
2635 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2636 return None
2637 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2638 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2639 # Short urls like https://domain/<issue_number> can be used, but don't allow
2640 # specifying the patchset (you'd 404), but we allow that here.
2641 if parsed_url.path == '/':
2642 part = parsed_url.fragment
2643 else:
2644 part = parsed_url.path
2645 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2646 if match:
2647 return _ParsedIssueNumberArgument(
2648 issue=int(match.group(2)),
2649 patchset=int(match.group(4)) if match.group(4) else None,
2650 hostname=parsed_url.netloc)
2651 return None
2652
tandrii16e0b4e2016-06-07 10:34:28 -07002653 def _GerritCommitMsgHookCheck(self, offer_removal):
2654 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2655 if not os.path.exists(hook):
2656 return
2657 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2658 # custom developer made one.
2659 data = gclient_utils.FileRead(hook)
2660 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2661 return
2662 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002663 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002664 'and may interfere with it in subtle ways.\n'
2665 'We recommend you remove the commit-msg hook.')
2666 if offer_removal:
2667 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2668 if reply.lower().startswith('y'):
2669 gclient_utils.rm_file_or_tree(hook)
2670 print('Gerrit commit-msg hook removed.')
2671 else:
2672 print('OK, will keep Gerrit commit-msg hook in place.')
2673
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002674 def CMDUploadChange(self, options, args, change):
2675 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002676 if options.squash and options.no_squash:
2677 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002678
2679 if not options.squash and not options.no_squash:
2680 # Load default for user, repo, squash=true, in this order.
2681 options.squash = settings.GetSquashGerritUploads()
2682 elif options.no_squash:
2683 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002684
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002685 # We assume the remote called "origin" is the one we want.
2686 # It is probably not worthwhile to support different workflows.
2687 gerrit_remote = 'origin'
2688
2689 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01002690 # Gerrit will not support pending prefix at all.
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002691 branch = GetTargetRef(remote, remote_branch, options.target_branch,
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01002692 pending_prefix_check=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002693
Aaron Gableb56ad332017-01-06 15:24:31 -08002694 # This may be None; default fallback value is determined in logic below.
2695 title = options.title
Aaron Gable856585d2017-01-23 15:32:00 -08002696 automatic_title = False
Aaron Gableb56ad332017-01-06 15:24:31 -08002697
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002698 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002699 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002700 if self.GetIssue():
2701 # Try to get the message from a previous upload.
2702 message = self.GetDescription()
2703 if not message:
2704 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002705 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002706 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002707 if not title:
2708 default_title = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
2709 title = ask_for_data(
2710 'Title for patchset [%s]: ' % default_title) or default_title
Aaron Gable856585d2017-01-23 15:32:00 -08002711 if title == default_title:
2712 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002713 change_id = self._GetChangeDetail()['change_id']
2714 while True:
2715 footer_change_ids = git_footers.get_footer_change_id(message)
2716 if footer_change_ids == [change_id]:
2717 break
2718 if not footer_change_ids:
2719 message = git_footers.add_footer_change_id(message, change_id)
Aaron Gablea45ee112016-11-22 15:14:38 -08002720 print('WARNING: appended missing Change-Id to change description')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002721 continue
2722 # There is already a valid footer but with different or several ids.
2723 # Doing this automatically is non-trivial as we don't want to lose
2724 # existing other footers, yet we want to append just 1 desired
2725 # Change-Id. Thus, just create a new footer, but let user verify the
2726 # new description.
2727 message = '%s\n\nChange-Id: %s' % (message, change_id)
2728 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002729 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002730 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002731 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002732 'Please, check the proposed correction to the description, '
2733 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2734 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2735 change_id))
2736 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2737 if not options.force:
2738 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002739 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002740 message = change_desc.description
2741 if not message:
2742 DieWithError("Description is empty. Aborting...")
2743 # Continue the while loop.
2744 # Sanity check of this code - we should end up with proper message
2745 # footer.
2746 assert [change_id] == git_footers.get_footer_change_id(message)
2747 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002748 else: # if not self.GetIssue()
2749 if options.message:
2750 message = options.message
2751 else:
2752 message = CreateDescriptionFromLog(args)
2753 if options.title:
2754 message = options.title + '\n\n' + message
2755 change_desc = ChangeDescription(message)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002756 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002757 change_desc.prompt(bug=options.bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002758 # On first upload, patchset title is always this string, while
2759 # --title flag gets converted to first line of message.
2760 title = 'Initial upload'
Aaron Gable856585d2017-01-23 15:32:00 -08002761 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002762 if not change_desc.description:
2763 DieWithError("Description is empty. Aborting...")
2764 message = change_desc.description
2765 change_ids = git_footers.get_footer_change_id(message)
2766 if len(change_ids) > 1:
2767 DieWithError('too many Change-Id footers, at most 1 allowed.')
2768 if not change_ids:
2769 # Generate the Change-Id automatically.
2770 message = git_footers.add_footer_change_id(
2771 message, GenerateGerritChangeId(message))
2772 change_desc.set_description(message)
2773 change_ids = git_footers.get_footer_change_id(message)
2774 assert len(change_ids) == 1
2775 change_id = change_ids[0]
2776
2777 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2778 if remote is '.':
2779 # If our upstream branch is local, we base our squashed commit on its
2780 # squashed version.
2781 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2782 # Check the squashed hash of the parent.
2783 parent = RunGit(['config',
2784 'branch.%s.gerritsquashhash' % upstream_branch_name],
2785 error_ok=True).strip()
2786 # Verify that the upstream branch has been uploaded too, otherwise
2787 # Gerrit will create additional CLs when uploading.
2788 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2789 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002790 DieWithError(
Aaron Gable48746652017-01-06 11:49:21 -08002791 '\nUpload upstream branch %s first.\n'
2792 'It is likely that this branch has been rebased since its last '
2793 'upload, so you just need to upload it again.\n'
2794 '(If you uploaded it with --no-squash, then branch dependencies '
2795 'are not supported, and you should reupload with --squash.)'
Christopher Lamf732cd52017-01-24 12:40:11 +11002796 % upstream_branch_name, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002797 else:
2798 parent = self.GetCommonAncestorWithUpstream()
2799
2800 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2801 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2802 '-m', message]).strip()
2803 else:
2804 change_desc = ChangeDescription(
2805 options.message or CreateDescriptionFromLog(args))
2806 if not change_desc.description:
2807 DieWithError("Description is empty. Aborting...")
2808
2809 if not git_footers.get_footer_change_id(change_desc.description):
2810 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002811 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2812 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002813 ref_to_push = 'HEAD'
2814 parent = '%s/%s' % (gerrit_remote, branch)
2815 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2816
2817 assert change_desc
2818 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2819 ref_to_push)]).splitlines()
2820 if len(commits) > 1:
2821 print('WARNING: This will upload %d commits. Run the following command '
2822 'to see which commits will be uploaded: ' % len(commits))
2823 print('git log %s..%s' % (parent, ref_to_push))
2824 print('You can also use `git squash-branch` to squash these into a '
2825 'single commit.')
2826 ask_for_data('About to upload; enter to confirm.')
2827
2828 if options.reviewers or options.tbr_owners:
2829 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2830 change)
2831
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002832 # Extra options that can be specified at push time. Doc:
2833 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2834 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002835 if change_desc.get_reviewers(tbr_only=True):
2836 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2837 refspec_opts.append('l=Code-Review+1')
2838
Aaron Gable9b713dd2016-12-14 16:04:21 -08002839 if title:
2840 if not re.match(r'^[\w ]+$', title):
2841 title = re.sub(r'[^\w ]', '', title)
Aaron Gable856585d2017-01-23 15:32:00 -08002842 if not automatic_title:
2843 print('WARNING: Patchset title may only contain alphanumeric chars '
2844 'and spaces. Cleaned up title:\n%s' % title)
2845 if not options.force:
2846 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002847 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2848 # reverse on its side.
Aaron Gable9b713dd2016-12-14 16:04:21 -08002849 refspec_opts.append('m=' + title.replace(' ', '_'))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002850
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002851 if options.send_mail:
2852 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002853 DieWithError('Must specify reviewers to send email.', change_desc)
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002854 refspec_opts.append('notify=ALL')
2855 else:
2856 refspec_opts.append('notify=NONE')
2857
tandrii99a72f22016-08-17 14:33:24 -07002858 reviewers = change_desc.get_reviewers()
2859 if reviewers:
2860 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002861
agablec6787972016-09-09 16:13:34 -07002862 if options.private:
2863 refspec_opts.append('draft')
2864
rmistry9eadede2016-09-19 11:22:43 -07002865 if options.topic:
2866 # Documentation on Gerrit topics is here:
2867 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2868 refspec_opts.append('topic=%s' % options.topic)
2869
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002870 refspec_suffix = ''
2871 if refspec_opts:
2872 refspec_suffix = '%' + ','.join(refspec_opts)
2873 assert ' ' not in refspec_suffix, (
2874 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002875 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002876
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002877 try:
2878 push_stdout = gclient_utils.CheckCallAndFilter(
2879 ['git', 'push', gerrit_remote, refspec],
2880 print_stdout=True,
2881 # Flush after every line: useful for seeing progress when running as
2882 # recipe.
2883 filter_fn=lambda _: sys.stdout.flush())
2884 except subprocess2.CalledProcessError:
2885 DieWithError('Failed to create a change. Please examine output above '
Christopher Lamf732cd52017-01-24 12:40:11 +11002886 'for the reason of the failure. ', change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002887
2888 if options.squash:
2889 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2890 change_numbers = [m.group(1)
2891 for m in map(regex.match, push_stdout.splitlines())
2892 if m]
2893 if len(change_numbers) != 1:
2894 DieWithError(
2895 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002896 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002897 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002898 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002899
2900 # Add cc's from the CC_LIST and --cc flag (if any).
2901 cc = self.GetCCList().split(',')
2902 if options.cc:
2903 cc.extend(options.cc)
2904 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002905 if change_desc.get_cced():
2906 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002907 if cc:
Aaron Gable5db82092016-11-17 10:52:08 -08002908 gerrit_util.AddReviewers(
Aaron Gable59f48512017-01-12 10:54:46 -08002909 self._GetGerritHost(), self.GetIssue(), cc,
2910 is_reviewer=False, notify=bool(options.send_mail))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002911 return 0
2912
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002913 def _AddChangeIdToCommitMessage(self, options, args):
2914 """Re-commits using the current message, assumes the commit hook is in
2915 place.
2916 """
2917 log_desc = options.message or CreateDescriptionFromLog(args)
2918 git_command = ['commit', '--amend', '-m', log_desc]
2919 RunGit(git_command)
2920 new_log_desc = CreateDescriptionFromLog(args)
2921 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002922 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002923 return new_log_desc
2924 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002925 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002926
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002927 def SetCQState(self, new_state):
2928 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002929 vote_map = {
2930 _CQState.NONE: 0,
2931 _CQState.DRY_RUN: 1,
2932 _CQState.COMMIT : 2,
2933 }
Andrii Shyshkalov828701b2016-12-09 10:46:47 +01002934 kwargs = {'labels': {'Commit-Queue': vote_map[new_state]}}
2935 if new_state == _CQState.DRY_RUN:
2936 # Don't spam everybody reviewer/owner.
2937 kwargs['notify'] = 'NONE'
2938 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(), **kwargs)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002939
tandriie113dfd2016-10-11 10:20:12 -07002940 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002941 try:
2942 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002943 except GerritChangeNotExists:
2944 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002945
2946 if data['status'] in ('ABANDONED', 'MERGED'):
2947 return 'CL %s is closed' % self.GetIssue()
2948
2949 def GetTryjobProperties(self, patchset=None):
2950 """Returns dictionary of properties to launch tryjob."""
2951 data = self._GetChangeDetail(['ALL_REVISIONS'])
2952 patchset = int(patchset or self.GetPatchset())
2953 assert patchset
2954 revision_data = None # Pylint wants it to be defined.
2955 for revision_data in data['revisions'].itervalues():
2956 if int(revision_data['_number']) == patchset:
2957 break
2958 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002959 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002960 (patchset, self.GetIssue()))
2961 return {
2962 'patch_issue': self.GetIssue(),
2963 'patch_set': patchset or self.GetPatchset(),
2964 'patch_project': data['project'],
2965 'patch_storage': 'gerrit',
2966 'patch_ref': revision_data['fetch']['http']['ref'],
2967 'patch_repository_url': revision_data['fetch']['http']['url'],
2968 'patch_gerrit_url': self.GetCodereviewServer(),
2969 }
tandriie113dfd2016-10-11 10:20:12 -07002970
tandriide281ae2016-10-12 06:02:30 -07002971 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002972 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002973
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002974
2975_CODEREVIEW_IMPLEMENTATIONS = {
2976 'rietveld': _RietveldChangelistImpl,
2977 'gerrit': _GerritChangelistImpl,
2978}
2979
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002980
iannuccie53c9352016-08-17 14:40:40 -07002981def _add_codereview_issue_select_options(parser, extra=""):
2982 _add_codereview_select_options(parser)
2983
2984 text = ('Operate on this issue number instead of the current branch\'s '
2985 'implicit issue.')
2986 if extra:
2987 text += ' '+extra
2988 parser.add_option('-i', '--issue', type=int, help=text)
2989
2990
2991def _process_codereview_issue_select_options(parser, options):
2992 _process_codereview_select_options(parser, options)
2993 if options.issue is not None and not options.forced_codereview:
2994 parser.error('--issue must be specified with either --rietveld or --gerrit')
2995
2996
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002997def _add_codereview_select_options(parser):
2998 """Appends --gerrit and --rietveld options to force specific codereview."""
2999 parser.codereview_group = optparse.OptionGroup(
3000 parser, 'EXPERIMENTAL! Codereview override options')
3001 parser.add_option_group(parser.codereview_group)
3002 parser.codereview_group.add_option(
3003 '--gerrit', action='store_true',
3004 help='Force the use of Gerrit for codereview')
3005 parser.codereview_group.add_option(
3006 '--rietveld', action='store_true',
3007 help='Force the use of Rietveld for codereview')
3008
3009
3010def _process_codereview_select_options(parser, options):
3011 if options.gerrit and options.rietveld:
3012 parser.error('Options --gerrit and --rietveld are mutually exclusive')
3013 options.forced_codereview = None
3014 if options.gerrit:
3015 options.forced_codereview = 'gerrit'
3016 elif options.rietveld:
3017 options.forced_codereview = 'rietveld'
3018
3019
tandriif9aefb72016-07-01 09:06:51 -07003020def _get_bug_line_values(default_project, bugs):
3021 """Given default_project and comma separated list of bugs, yields bug line
3022 values.
3023
3024 Each bug can be either:
3025 * a number, which is combined with default_project
3026 * string, which is left as is.
3027
3028 This function may produce more than one line, because bugdroid expects one
3029 project per line.
3030
3031 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3032 ['v8:123', 'chromium:789']
3033 """
3034 default_bugs = []
3035 others = []
3036 for bug in bugs.split(','):
3037 bug = bug.strip()
3038 if bug:
3039 try:
3040 default_bugs.append(int(bug))
3041 except ValueError:
3042 others.append(bug)
3043
3044 if default_bugs:
3045 default_bugs = ','.join(map(str, default_bugs))
3046 if default_project:
3047 yield '%s:%s' % (default_project, default_bugs)
3048 else:
3049 yield default_bugs
3050 for other in sorted(others):
3051 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3052 yield other
3053
3054
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003055class ChangeDescription(object):
3056 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003057 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003058 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00003059 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003060 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003061
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003062 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003063 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003064
agable@chromium.org42c20792013-09-12 17:34:49 +00003065 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003066 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003067 return '\n'.join(self._description_lines)
3068
3069 def set_description(self, desc):
3070 if isinstance(desc, basestring):
3071 lines = desc.splitlines()
3072 else:
3073 lines = [line.rstrip() for line in desc]
3074 while lines and not lines[0]:
3075 lines.pop(0)
3076 while lines and not lines[-1]:
3077 lines.pop(-1)
3078 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003079
piman@chromium.org336f9122014-09-04 02:16:55 +00003080 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003081 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003082 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00003083 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003084 return
agable@chromium.org42c20792013-09-12 17:34:49 +00003085 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003086
agable@chromium.org42c20792013-09-12 17:34:49 +00003087 # Get the set of R= and TBR= lines and remove them from the desciption.
3088 regexp = re.compile(self.R_LINE)
3089 matches = [regexp.match(line) for line in self._description_lines]
3090 new_desc = [l for i, l in enumerate(self._description_lines)
3091 if not matches[i]]
3092 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003093
agable@chromium.org42c20792013-09-12 17:34:49 +00003094 # Construct new unified R= and TBR= lines.
3095 r_names = []
3096 tbr_names = []
3097 for match in matches:
3098 if not match:
3099 continue
3100 people = cleanup_list([match.group(2).strip()])
3101 if match.group(1) == 'TBR':
3102 tbr_names.extend(people)
3103 else:
3104 r_names.extend(people)
3105 for name in r_names:
3106 if name not in reviewers:
3107 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003108 if add_owners_tbr:
3109 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003110 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003111 all_reviewers = set(tbr_names + reviewers)
3112 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3113 all_reviewers)
3114 tbr_names.extend(owners_db.reviewers_for(missing_files,
3115 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003116 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3117 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3118
3119 # Put the new lines in the description where the old first R= line was.
3120 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3121 if 0 <= line_loc < len(self._description_lines):
3122 if new_tbr_line:
3123 self._description_lines.insert(line_loc, new_tbr_line)
3124 if new_r_line:
3125 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003126 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003127 if new_r_line:
3128 self.append_footer(new_r_line)
3129 if new_tbr_line:
3130 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003131
tandriif9aefb72016-07-01 09:06:51 -07003132 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003133 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003134 self.set_description([
3135 '# Enter a description of the change.',
3136 '# This will be displayed on the codereview site.',
3137 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003138 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003139 '--------------------',
3140 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003141
agable@chromium.org42c20792013-09-12 17:34:49 +00003142 regexp = re.compile(self.BUG_LINE)
3143 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003144 prefix = settings.GetBugPrefix()
3145 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
3146 for value in values:
3147 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
3148 self.append_footer('BUG=%s' % value)
3149
agable@chromium.org42c20792013-09-12 17:34:49 +00003150 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003151 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003152 if not content:
3153 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003154 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003155
3156 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003157 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3158 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003159 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003160 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003161
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003162 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003163 """Adds a footer line to the description.
3164
3165 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3166 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3167 that Gerrit footers are always at the end.
3168 """
3169 parsed_footer_line = git_footers.parse_footer(line)
3170 if parsed_footer_line:
3171 # Line is a gerrit footer in the form: Footer-Key: any value.
3172 # Thus, must be appended observing Gerrit footer rules.
3173 self.set_description(
3174 git_footers.add_footer(self.description,
3175 key=parsed_footer_line[0],
3176 value=parsed_footer_line[1]))
3177 return
3178
3179 if not self._description_lines:
3180 self._description_lines.append(line)
3181 return
3182
3183 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3184 if gerrit_footers:
3185 # git_footers.split_footers ensures that there is an empty line before
3186 # actual (gerrit) footers, if any. We have to keep it that way.
3187 assert top_lines and top_lines[-1] == ''
3188 top_lines, separator = top_lines[:-1], top_lines[-1:]
3189 else:
3190 separator = [] # No need for separator if there are no gerrit_footers.
3191
3192 prev_line = top_lines[-1] if top_lines else ''
3193 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3194 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3195 top_lines.append('')
3196 top_lines.append(line)
3197 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003198
tandrii99a72f22016-08-17 14:33:24 -07003199 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003200 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003201 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003202 reviewers = [match.group(2).strip()
3203 for match in matches
3204 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003205 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003206
bradnelsond975b302016-10-23 12:20:23 -07003207 def get_cced(self):
3208 """Retrieves the list of reviewers."""
3209 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3210 cced = [match.group(2).strip() for match in matches if match]
3211 return cleanup_list(cced)
3212
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003213 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3214 """Updates this commit description given the parent.
3215
3216 This is essentially what Gnumbd used to do.
3217 Consult https://goo.gl/WMmpDe for more details.
3218 """
3219 assert parent_msg # No, orphan branch creation isn't supported.
3220 assert parent_hash
3221 assert dest_ref
3222 parent_footer_map = git_footers.parse_footers(parent_msg)
3223 # This will also happily parse svn-position, which GnumbD is no longer
3224 # supporting. While we'd generate correct footers, the verifier plugin
3225 # installed in Gerrit will block such commit (ie git push below will fail).
3226 parent_position = git_footers.get_position(parent_footer_map)
3227
3228 # Cherry-picks may have last line obscuring their prior footers,
3229 # from git_footers perspective. This is also what Gnumbd did.
3230 cp_line = None
3231 if (self._description_lines and
3232 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3233 cp_line = self._description_lines.pop()
3234
3235 top_lines, _, parsed_footers = git_footers.split_footers(self.description)
3236
3237 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3238 # user interference with actual footers we'd insert below.
3239 for i, (k, v) in enumerate(parsed_footers):
3240 if k.startswith('Cr-'):
3241 parsed_footers[i] = (k.replace('Cr-', 'Cr-Original-'), v)
3242
3243 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003244 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003245 if parent_position[0] == dest_ref:
3246 # Same branch as parent.
3247 number = int(parent_position[1]) + 1
3248 else:
3249 number = 1 # New branch, and extra lineage.
3250 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3251 int(parent_position[1])))
3252
3253 parsed_footers.append(('Cr-Commit-Position',
3254 '%s@{#%d}' % (dest_ref, number)))
3255 parsed_footers.extend(('Cr-Branched-From', v) for v in lineage)
3256
3257 self._description_lines = top_lines
3258 if cp_line:
3259 self._description_lines.append(cp_line)
3260 if self._description_lines[-1] != '':
3261 self._description_lines.append('') # Ensure footer separator.
3262 self._description_lines.extend('%s: %s' % kv for kv in parsed_footers)
3263
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003264
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003265def get_approving_reviewers(props):
3266 """Retrieves the reviewers that approved a CL from the issue properties with
3267 messages.
3268
3269 Note that the list may contain reviewers that are not committer, thus are not
3270 considered by the CQ.
3271 """
3272 return sorted(
3273 set(
3274 message['sender']
3275 for message in props['messages']
3276 if message['approval'] and message['sender'] in props['reviewers']
3277 )
3278 )
3279
3280
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003281def FindCodereviewSettingsFile(filename='codereview.settings'):
3282 """Finds the given file starting in the cwd and going up.
3283
3284 Only looks up to the top of the repository unless an
3285 'inherit-review-settings-ok' file exists in the root of the repository.
3286 """
3287 inherit_ok_file = 'inherit-review-settings-ok'
3288 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003289 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003290 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3291 root = '/'
3292 while True:
3293 if filename in os.listdir(cwd):
3294 if os.path.isfile(os.path.join(cwd, filename)):
3295 return open(os.path.join(cwd, filename))
3296 if cwd == root:
3297 break
3298 cwd = os.path.dirname(cwd)
3299
3300
3301def LoadCodereviewSettingsFromFile(fileobj):
3302 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003303 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003304
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003305 def SetProperty(name, setting, unset_error_ok=False):
3306 fullname = 'rietveld.' + name
3307 if setting in keyvals:
3308 RunGit(['config', fullname, keyvals[setting]])
3309 else:
3310 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3311
tandrii48df5812016-10-17 03:55:37 -07003312 if not keyvals.get('GERRIT_HOST', False):
3313 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003314 # Only server setting is required. Other settings can be absent.
3315 # In that case, we ignore errors raised during option deletion attempt.
3316 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003317 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003318 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3319 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003320 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003321 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3322 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003323 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003324 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003325 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3326 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003327
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003328 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003329 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003330
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003331 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003332 RunGit(['config', 'gerrit.squash-uploads',
3333 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003334
tandrii@chromium.org28253532016-04-14 13:46:56 +00003335 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003336 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003337 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3338
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003339 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3340 #should be of the form
3341 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3342 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
3343 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3344 keyvals['ORIGIN_URL_CONFIG']])
3345
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003346
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003347def urlretrieve(source, destination):
3348 """urllib is broken for SSL connections via a proxy therefore we
3349 can't use urllib.urlretrieve()."""
3350 with open(destination, 'w') as f:
3351 f.write(urllib2.urlopen(source).read())
3352
3353
ukai@chromium.org712d6102013-11-27 00:52:58 +00003354def hasSheBang(fname):
3355 """Checks fname is a #! script."""
3356 with open(fname) as f:
3357 return f.read(2).startswith('#!')
3358
3359
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003360# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3361def DownloadHooks(*args, **kwargs):
3362 pass
3363
3364
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003365def DownloadGerritHook(force):
3366 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003367
3368 Args:
3369 force: True to update hooks. False to install hooks if not present.
3370 """
3371 if not settings.GetIsGerrit():
3372 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003373 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003374 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3375 if not os.access(dst, os.X_OK):
3376 if os.path.exists(dst):
3377 if not force:
3378 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003379 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003380 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003381 if not hasSheBang(dst):
3382 DieWithError('Not a script: %s\n'
3383 'You need to download from\n%s\n'
3384 'into .git/hooks/commit-msg and '
3385 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003386 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3387 except Exception:
3388 if os.path.exists(dst):
3389 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003390 DieWithError('\nFailed to download hooks.\n'
3391 'You need to download from\n%s\n'
3392 'into .git/hooks/commit-msg and '
3393 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003394
3395
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003396
3397def GetRietveldCodereviewSettingsInteractively():
3398 """Prompt the user for settings."""
3399 server = settings.GetDefaultServerUrl(error_ok=True)
3400 prompt = 'Rietveld server (host[:port])'
3401 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3402 newserver = ask_for_data(prompt + ':')
3403 if not server and not newserver:
3404 newserver = DEFAULT_SERVER
3405 if newserver:
3406 newserver = gclient_utils.UpgradeToHttps(newserver)
3407 if newserver != server:
3408 RunGit(['config', 'rietveld.server', newserver])
3409
3410 def SetProperty(initial, caption, name, is_url):
3411 prompt = caption
3412 if initial:
3413 prompt += ' ("x" to clear) [%s]' % initial
3414 new_val = ask_for_data(prompt + ':')
3415 if new_val == 'x':
3416 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3417 elif new_val:
3418 if is_url:
3419 new_val = gclient_utils.UpgradeToHttps(new_val)
3420 if new_val != initial:
3421 RunGit(['config', 'rietveld.' + name, new_val])
3422
3423 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3424 SetProperty(settings.GetDefaultPrivateFlag(),
3425 'Private flag (rietveld only)', 'private', False)
3426 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3427 'tree-status-url', False)
3428 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3429 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3430 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3431 'run-post-upload-hook', False)
3432
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003433@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003434def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003435 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003436
tandrii5d0a0422016-09-14 06:24:35 -07003437 print('WARNING: git cl config works for Rietveld only')
3438 # TODO(tandrii): remove this once we switch to Gerrit.
3439 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003440 parser.add_option('--activate-update', action='store_true',
3441 help='activate auto-updating [rietveld] section in '
3442 '.git/config')
3443 parser.add_option('--deactivate-update', action='store_true',
3444 help='deactivate auto-updating [rietveld] section in '
3445 '.git/config')
3446 options, args = parser.parse_args(args)
3447
3448 if options.deactivate_update:
3449 RunGit(['config', 'rietveld.autoupdate', 'false'])
3450 return
3451
3452 if options.activate_update:
3453 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3454 return
3455
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003456 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003457 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003458 return 0
3459
3460 url = args[0]
3461 if not url.endswith('codereview.settings'):
3462 url = os.path.join(url, 'codereview.settings')
3463
3464 # Load code review settings and download hooks (if available).
3465 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3466 return 0
3467
3468
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003469def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003470 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003471 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3472 branch = ShortBranchName(branchref)
3473 _, args = parser.parse_args(args)
3474 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003475 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003476 return RunGit(['config', 'branch.%s.base-url' % branch],
3477 error_ok=False).strip()
3478 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003479 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003480 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3481 error_ok=False).strip()
3482
3483
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003484def color_for_status(status):
3485 """Maps a Changelist status to color, for CMDstatus and other tools."""
3486 return {
3487 'unsent': Fore.RED,
3488 'waiting': Fore.BLUE,
3489 'reply': Fore.YELLOW,
3490 'lgtm': Fore.GREEN,
3491 'commit': Fore.MAGENTA,
3492 'closed': Fore.CYAN,
3493 'error': Fore.WHITE,
3494 }.get(status, Fore.WHITE)
3495
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003496
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003497def get_cl_statuses(changes, fine_grained, max_processes=None):
3498 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003499
3500 If fine_grained is true, this will fetch CL statuses from the server.
3501 Otherwise, simply indicate if there's a matching url for the given branches.
3502
3503 If max_processes is specified, it is used as the maximum number of processes
3504 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3505 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003506
3507 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003508 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003509 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003510 upload.verbosity = 0
3511
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003512 if not changes:
3513 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003514
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003515 if not fine_grained:
3516 # Fast path which doesn't involve querying codereview servers.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003517 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003518 for cl in changes:
3519 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003520 return
3521
3522 # First, sort out authentication issues.
3523 logging.debug('ensuring credentials exist')
3524 for cl in changes:
3525 cl.EnsureAuthenticated(force=False, refresh=True)
3526
3527 def fetch(cl):
3528 try:
3529 return (cl, cl.GetStatus())
3530 except:
3531 # See http://crbug.com/629863.
3532 logging.exception('failed to fetch status for %s:', cl)
3533 raise
3534
3535 threads_count = len(changes)
3536 if max_processes:
3537 threads_count = max(1, min(threads_count, max_processes))
3538 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3539
3540 pool = ThreadPool(threads_count)
3541 fetched_cls = set()
3542 try:
3543 it = pool.imap_unordered(fetch, changes).__iter__()
3544 while True:
3545 try:
3546 cl, status = it.next(timeout=5)
3547 except multiprocessing.TimeoutError:
3548 break
3549 fetched_cls.add(cl)
3550 yield cl, status
3551 finally:
3552 pool.close()
3553
3554 # Add any branches that failed to fetch.
3555 for cl in set(changes) - fetched_cls:
3556 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003557
rmistry@google.com2dd99862015-06-22 12:22:18 +00003558
3559def upload_branch_deps(cl, args):
3560 """Uploads CLs of local branches that are dependents of the current branch.
3561
3562 If the local branch dependency tree looks like:
3563 test1 -> test2.1 -> test3.1
3564 -> test3.2
3565 -> test2.2 -> test3.3
3566
3567 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3568 run on the dependent branches in this order:
3569 test2.1, test3.1, test3.2, test2.2, test3.3
3570
3571 Note: This function does not rebase your local dependent branches. Use it when
3572 you make a change to the parent branch that will not conflict with its
3573 dependent branches, and you would like their dependencies updated in
3574 Rietveld.
3575 """
3576 if git_common.is_dirty_git_tree('upload-branch-deps'):
3577 return 1
3578
3579 root_branch = cl.GetBranch()
3580 if root_branch is None:
3581 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3582 'Get on a branch!')
3583 if not cl.GetIssue() or not cl.GetPatchset():
3584 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3585 'patchset dependencies without an uploaded CL.')
3586
3587 branches = RunGit(['for-each-ref',
3588 '--format=%(refname:short) %(upstream:short)',
3589 'refs/heads'])
3590 if not branches:
3591 print('No local branches found.')
3592 return 0
3593
3594 # Create a dictionary of all local branches to the branches that are dependent
3595 # on it.
3596 tracked_to_dependents = collections.defaultdict(list)
3597 for b in branches.splitlines():
3598 tokens = b.split()
3599 if len(tokens) == 2:
3600 branch_name, tracked = tokens
3601 tracked_to_dependents[tracked].append(branch_name)
3602
vapiera7fbd5a2016-06-16 09:17:49 -07003603 print()
3604 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003605 dependents = []
3606 def traverse_dependents_preorder(branch, padding=''):
3607 dependents_to_process = tracked_to_dependents.get(branch, [])
3608 padding += ' '
3609 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003610 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003611 dependents.append(dependent)
3612 traverse_dependents_preorder(dependent, padding)
3613 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003614 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003615
3616 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003617 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003618 return 0
3619
vapiera7fbd5a2016-06-16 09:17:49 -07003620 print('This command will checkout all dependent branches and run '
3621 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003622 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3623
andybons@chromium.org962f9462016-02-03 20:00:42 +00003624 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003625 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003626 args.extend(['-t', 'Updated patchset dependency'])
3627
rmistry@google.com2dd99862015-06-22 12:22:18 +00003628 # Record all dependents that failed to upload.
3629 failures = {}
3630 # Go through all dependents, checkout the branch and upload.
3631 try:
3632 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003633 print()
3634 print('--------------------------------------')
3635 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003636 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003637 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003638 try:
3639 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003640 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003641 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003642 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003643 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003644 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003645 finally:
3646 # Swap back to the original root branch.
3647 RunGit(['checkout', '-q', root_branch])
3648
vapiera7fbd5a2016-06-16 09:17:49 -07003649 print()
3650 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003651 for dependent_branch in dependents:
3652 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003653 print(' %s : %s' % (dependent_branch, upload_status))
3654 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003655
3656 return 0
3657
3658
kmarshall3bff56b2016-06-06 18:31:47 -07003659def CMDarchive(parser, args):
3660 """Archives and deletes branches associated with closed changelists."""
3661 parser.add_option(
3662 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003663 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003664 parser.add_option(
3665 '-f', '--force', action='store_true',
3666 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003667 parser.add_option(
3668 '-d', '--dry-run', action='store_true',
3669 help='Skip the branch tagging and removal steps.')
3670 parser.add_option(
3671 '-t', '--notags', action='store_true',
3672 help='Do not tag archived branches. '
3673 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003674
3675 auth.add_auth_options(parser)
3676 options, args = parser.parse_args(args)
3677 if args:
3678 parser.error('Unsupported args: %s' % ' '.join(args))
3679 auth_config = auth.extract_auth_config_from_options(options)
3680
3681 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3682 if not branches:
3683 return 0
3684
vapiera7fbd5a2016-06-16 09:17:49 -07003685 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003686 changes = [Changelist(branchref=b, auth_config=auth_config)
3687 for b in branches.splitlines()]
3688 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3689 statuses = get_cl_statuses(changes,
3690 fine_grained=True,
3691 max_processes=options.maxjobs)
3692 proposal = [(cl.GetBranch(),
3693 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3694 for cl, status in statuses
3695 if status == 'closed']
3696 proposal.sort()
3697
3698 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003699 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003700 return 0
3701
3702 current_branch = GetCurrentBranch()
3703
vapiera7fbd5a2016-06-16 09:17:49 -07003704 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003705 if options.notags:
3706 for next_item in proposal:
3707 print(' ' + next_item[0])
3708 else:
3709 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3710 for next_item in proposal:
3711 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003712
kmarshall9249e012016-08-23 12:02:16 -07003713 # Quit now on precondition failure or if instructed by the user, either
3714 # via an interactive prompt or by command line flags.
3715 if options.dry_run:
3716 print('\nNo changes were made (dry run).\n')
3717 return 0
3718 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003719 print('You are currently on a branch \'%s\' which is associated with a '
3720 'closed codereview issue, so archive cannot proceed. Please '
3721 'checkout another branch and run this command again.' %
3722 current_branch)
3723 return 1
kmarshall9249e012016-08-23 12:02:16 -07003724 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003725 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3726 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003727 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003728 return 1
3729
3730 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003731 if not options.notags:
3732 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003733 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003734
vapiera7fbd5a2016-06-16 09:17:49 -07003735 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003736
3737 return 0
3738
3739
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003740def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003741 """Show status of changelists.
3742
3743 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003744 - Red not sent for review or broken
3745 - Blue waiting for review
3746 - Yellow waiting for you to reply to review
3747 - Green LGTM'ed
3748 - Magenta in the commit queue
3749 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003750
3751 Also see 'git cl comments'.
3752 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003753 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003754 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003755 parser.add_option('-f', '--fast', action='store_true',
3756 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003757 parser.add_option(
3758 '-j', '--maxjobs', action='store', type=int,
3759 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003760
3761 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003762 _add_codereview_issue_select_options(
3763 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003764 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003765 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003766 if args:
3767 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003768 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003769
iannuccie53c9352016-08-17 14:40:40 -07003770 if options.issue is not None and not options.field:
3771 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003772
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003773 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003774 cl = Changelist(auth_config=auth_config, issue=options.issue,
3775 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003776 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003777 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003778 elif options.field == 'id':
3779 issueid = cl.GetIssue()
3780 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003781 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003782 elif options.field == 'patch':
3783 patchset = cl.GetPatchset()
3784 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003785 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003786 elif options.field == 'status':
3787 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003788 elif options.field == 'url':
3789 url = cl.GetIssueURL()
3790 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003791 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003792 return 0
3793
3794 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3795 if not branches:
3796 print('No local branch found.')
3797 return 0
3798
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003799 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003800 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003801 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003802 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003803 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003804 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003805 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003806
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003807 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003808 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3809 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3810 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003811 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003812 c, status = output.next()
3813 branch_statuses[c.GetBranch()] = status
3814 status = branch_statuses.pop(branch)
3815 url = cl.GetIssueURL()
3816 if url and (not status or status == 'error'):
3817 # The issue probably doesn't exist anymore.
3818 url += ' (broken)'
3819
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003820 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003821 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003822 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003823 color = ''
3824 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003825 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003826 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003827 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003828 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003829
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003830
3831 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07003832 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003833 print('Current branch: %s' % branch)
3834 for cl in changes:
3835 if cl.GetBranch() == branch:
3836 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003837 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003838 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003839 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003840 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003841 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003842 print('Issue description:')
3843 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003844 return 0
3845
3846
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003847def colorize_CMDstatus_doc():
3848 """To be called once in main() to add colors to git cl status help."""
3849 colors = [i for i in dir(Fore) if i[0].isupper()]
3850
3851 def colorize_line(line):
3852 for color in colors:
3853 if color in line.upper():
3854 # Extract whitespaces first and the leading '-'.
3855 indent = len(line) - len(line.lstrip(' ')) + 1
3856 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3857 return line
3858
3859 lines = CMDstatus.__doc__.splitlines()
3860 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3861
3862
phajdan.jre328cf92016-08-22 04:12:17 -07003863def write_json(path, contents):
3864 with open(path, 'w') as f:
3865 json.dump(contents, f)
3866
3867
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003868@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003869def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003870 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003871
3872 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003873 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003874 parser.add_option('-r', '--reverse', action='store_true',
3875 help='Lookup the branch(es) for the specified issues. If '
3876 'no issues are specified, all branches with mapped '
3877 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003878 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003879 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003880 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003881 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003882
dnj@chromium.org406c4402015-03-03 17:22:28 +00003883 if options.reverse:
3884 branches = RunGit(['for-each-ref', 'refs/heads',
3885 '--format=%(refname:short)']).splitlines()
3886
3887 # Reverse issue lookup.
3888 issue_branch_map = {}
3889 for branch in branches:
3890 cl = Changelist(branchref=branch)
3891 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3892 if not args:
3893 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003894 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003895 for issue in args:
3896 if not issue:
3897 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003898 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003899 print('Branch for issue number %s: %s' % (
3900 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003901 if options.json:
3902 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003903 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003904 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003905 if len(args) > 0:
3906 try:
3907 issue = int(args[0])
3908 except ValueError:
3909 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003910 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003911 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003912 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003913 if options.json:
3914 write_json(options.json, {
3915 'issue': cl.GetIssue(),
3916 'issue_url': cl.GetIssueURL(),
3917 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003918 return 0
3919
3920
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003921def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003922 """Shows or posts review comments for any changelist."""
3923 parser.add_option('-a', '--add-comment', dest='comment',
3924 help='comment to add to an issue')
3925 parser.add_option('-i', dest='issue',
3926 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003927 parser.add_option('-j', '--json-file',
3928 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003929 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003930 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003931 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003932
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003933 issue = None
3934 if options.issue:
3935 try:
3936 issue = int(options.issue)
3937 except ValueError:
3938 DieWithError('A review issue id is expected to be a number')
3939
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003940 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003941
3942 if options.comment:
3943 cl.AddComment(options.comment)
3944 return 0
3945
3946 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003947 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003948 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003949 summary.append({
3950 'date': message['date'],
3951 'lgtm': False,
3952 'message': message['text'],
3953 'not_lgtm': False,
3954 'sender': message['sender'],
3955 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003956 if message['disapproval']:
3957 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003958 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003959 elif message['approval']:
3960 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003961 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003962 elif message['sender'] == data['owner_email']:
3963 color = Fore.MAGENTA
3964 else:
3965 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003966 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003967 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003968 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003969 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003970 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003971 if options.json_file:
3972 with open(options.json_file, 'wb') as f:
3973 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003974 return 0
3975
3976
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003977@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003978def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003979 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003980 parser.add_option('-d', '--display', action='store_true',
3981 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003982 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003983 help='New description to set for this issue (- for stdin, '
3984 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003985 parser.add_option('-f', '--force', action='store_true',
3986 help='Delete any unpublished Gerrit edits for this issue '
3987 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003988
3989 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003990 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003991 options, args = parser.parse_args(args)
3992 _process_codereview_select_options(parser, options)
3993
3994 target_issue = None
3995 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003996 target_issue = ParseIssueNumberArgument(args[0])
3997 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003998 parser.print_help()
3999 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004000
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004001 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004002
martiniss6eda05f2016-06-30 10:18:35 -07004003 kwargs = {
4004 'auth_config': auth_config,
4005 'codereview': options.forced_codereview,
4006 }
4007 if target_issue:
4008 kwargs['issue'] = target_issue.issue
4009 if options.forced_codereview == 'rietveld':
4010 kwargs['rietveld_server'] = target_issue.hostname
4011
4012 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004013
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004014 if not cl.GetIssue():
4015 DieWithError('This branch has no associated changelist.')
4016 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004017
smut@google.com34fb6b12015-07-13 20:03:26 +00004018 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004019 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004020 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004021
4022 if options.new_description:
4023 text = options.new_description
4024 if text == '-':
4025 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004026 elif text == '+':
4027 base_branch = cl.GetCommonAncestorWithUpstream()
4028 change = cl.GetChange(base_branch, None, local_description=True)
4029 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004030
4031 description.set_description(text)
4032 else:
4033 description.prompt()
4034
wychen@chromium.org063e4e52015-04-03 06:51:44 +00004035 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004036 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004037 return 0
4038
4039
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004040def CreateDescriptionFromLog(args):
4041 """Pulls out the commit log to use as a base for the CL description."""
4042 log_args = []
4043 if len(args) == 1 and not args[0].endswith('.'):
4044 log_args = [args[0] + '..']
4045 elif len(args) == 1 and args[0].endswith('...'):
4046 log_args = [args[0][:-1]]
4047 elif len(args) == 2:
4048 log_args = [args[0] + '..' + args[1]]
4049 else:
4050 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004051 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004052
4053
thestig@chromium.org44202a22014-03-11 19:22:18 +00004054def CMDlint(parser, args):
4055 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004056 parser.add_option('--filter', action='append', metavar='-x,+y',
4057 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004058 auth.add_auth_options(parser)
4059 options, args = parser.parse_args(args)
4060 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004061
4062 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004063 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004064 try:
4065 import cpplint
4066 import cpplint_chromium
4067 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004068 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004069 return 1
4070
4071 # Change the current working directory before calling lint so that it
4072 # shows the correct base.
4073 previous_cwd = os.getcwd()
4074 os.chdir(settings.GetRoot())
4075 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004076 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004077 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4078 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004079 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004080 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004081 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004082
4083 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004084 command = args + files
4085 if options.filter:
4086 command = ['--filter=' + ','.join(options.filter)] + command
4087 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004088
4089 white_regex = re.compile(settings.GetLintRegex())
4090 black_regex = re.compile(settings.GetLintIgnoreRegex())
4091 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4092 for filename in filenames:
4093 if white_regex.match(filename):
4094 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004095 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004096 else:
4097 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4098 extra_check_functions)
4099 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004100 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004101 finally:
4102 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004103 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004104 if cpplint._cpplint_state.error_count != 0:
4105 return 1
4106 return 0
4107
4108
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004109def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004110 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004111 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004112 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004113 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004114 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004115 auth.add_auth_options(parser)
4116 options, args = parser.parse_args(args)
4117 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004118
sbc@chromium.org71437c02015-04-09 19:29:40 +00004119 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004120 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004121 return 1
4122
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004123 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004124 if args:
4125 base_branch = args[0]
4126 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004127 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004128 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004129
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004130 cl.RunHook(
4131 committing=not options.upload,
4132 may_prompt=False,
4133 verbose=options.verbose,
4134 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004135 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004136
4137
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004138def GenerateGerritChangeId(message):
4139 """Returns Ixxxxxx...xxx change id.
4140
4141 Works the same way as
4142 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4143 but can be called on demand on all platforms.
4144
4145 The basic idea is to generate git hash of a state of the tree, original commit
4146 message, author/committer info and timestamps.
4147 """
4148 lines = []
4149 tree_hash = RunGitSilent(['write-tree'])
4150 lines.append('tree %s' % tree_hash.strip())
4151 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4152 if code == 0:
4153 lines.append('parent %s' % parent.strip())
4154 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4155 lines.append('author %s' % author.strip())
4156 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4157 lines.append('committer %s' % committer.strip())
4158 lines.append('')
4159 # Note: Gerrit's commit-hook actually cleans message of some lines and
4160 # whitespace. This code is not doing this, but it clearly won't decrease
4161 # entropy.
4162 lines.append(message)
4163 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4164 stdin='\n'.join(lines))
4165 return 'I%s' % change_hash.strip()
4166
4167
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004168def GetTargetRef(remote, remote_branch, target_branch, pending_prefix_check,
4169 remote_url=None):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004170 """Computes the remote branch ref to use for the CL.
4171
4172 Args:
4173 remote (str): The git remote for the CL.
4174 remote_branch (str): The git remote branch for the CL.
4175 target_branch (str): The target branch specified by the user.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004176 pending_prefix_check (bool): If true, determines if pending_prefix should be
4177 used.
4178 remote_url (str): Only used for checking if pending_prefix should be used.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004179 """
4180 if not (remote and remote_branch):
4181 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004182
wittman@chromium.org455dc922015-01-26 20:15:50 +00004183 if target_branch:
4184 # Cannonicalize branch references to the equivalent local full symbolic
4185 # refs, which are then translated into the remote full symbolic refs
4186 # below.
4187 if '/' not in target_branch:
4188 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4189 else:
4190 prefix_replacements = (
4191 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4192 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4193 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4194 )
4195 match = None
4196 for regex, replacement in prefix_replacements:
4197 match = re.search(regex, target_branch)
4198 if match:
4199 remote_branch = target_branch.replace(match.group(0), replacement)
4200 break
4201 if not match:
4202 # This is a branch path but not one we recognize; use as-is.
4203 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004204 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4205 # Handle the refs that need to land in different refs.
4206 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004207
wittman@chromium.org455dc922015-01-26 20:15:50 +00004208 # Create the true path to the remote branch.
4209 # Does the following translation:
4210 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4211 # * refs/remotes/origin/master -> refs/heads/master
4212 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4213 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4214 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4215 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4216 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4217 'refs/heads/')
4218 elif remote_branch.startswith('refs/remotes/branch-heads'):
4219 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004220
4221 if pending_prefix_check:
4222 # If a pending prefix exists then replace refs/ with it.
4223 state = _GitNumbererState.load(remote_url, remote_branch)
4224 if state.pending_prefix:
4225 remote_branch = remote_branch.replace('refs/', state.pending_prefix)
wittman@chromium.org455dc922015-01-26 20:15:50 +00004226 return remote_branch
4227
4228
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004229def cleanup_list(l):
4230 """Fixes a list so that comma separated items are put as individual items.
4231
4232 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4233 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4234 """
4235 items = sum((i.split(',') for i in l), [])
4236 stripped_items = (i.strip() for i in items)
4237 return sorted(filter(None, stripped_items))
4238
4239
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004240@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004241def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004242 """Uploads the current changelist to codereview.
4243
4244 Can skip dependency patchset uploads for a branch by running:
4245 git config branch.branch_name.skip-deps-uploads True
4246 To unset run:
4247 git config --unset branch.branch_name.skip-deps-uploads
4248 Can also set the above globally by using the --global flag.
4249 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004250 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4251 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004252 parser.add_option('--bypass-watchlists', action='store_true',
4253 dest='bypass_watchlists',
4254 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004255 parser.add_option('-f', action='store_true', dest='force',
4256 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004257 parser.add_option('--message', '-m', dest='message',
4258 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004259 parser.add_option('-b', '--bug',
4260 help='pre-populate the bug number(s) for this issue. '
4261 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004262 parser.add_option('--message-file', dest='message_file',
4263 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004264 parser.add_option('--title', '-t', dest='title',
4265 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004266 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004267 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004268 help='reviewer email addresses')
4269 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004270 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004271 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004272 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004273 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004274 parser.add_option('--emulate_svn_auto_props',
4275 '--emulate-svn-auto-props',
4276 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004277 dest="emulate_svn_auto_props",
4278 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004279 parser.add_option('-c', '--use-commit-queue', action='store_true',
4280 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004281 parser.add_option('--private', action='store_true',
4282 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004283 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004284 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004285 metavar='TARGET',
4286 help='Apply CL to remote ref TARGET. ' +
4287 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004288 parser.add_option('--squash', action='store_true',
4289 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004290 parser.add_option('--no-squash', action='store_true',
4291 help='Don\'t squash multiple commits into one ' +
4292 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004293 parser.add_option('--topic', default=None,
4294 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004295 parser.add_option('--email', default=None,
4296 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004297 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4298 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004299 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4300 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004301 help='Send the patchset to do a CQ dry run right after '
4302 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004303 parser.add_option('--dependencies', action='store_true',
4304 help='Uploads CLs of all the local branches that depend on '
4305 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004306
rmistry@google.com2dd99862015-06-22 12:22:18 +00004307 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004308 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004309 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004310 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004311 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004312 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004313 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004314
sbc@chromium.org71437c02015-04-09 19:29:40 +00004315 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004316 return 1
4317
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004318 options.reviewers = cleanup_list(options.reviewers)
4319 options.cc = cleanup_list(options.cc)
4320
tandriib80458a2016-06-23 12:20:07 -07004321 if options.message_file:
4322 if options.message:
4323 parser.error('only one of --message and --message-file allowed.')
4324 options.message = gclient_utils.FileRead(options.message_file)
4325 options.message_file = None
4326
tandrii4d0545a2016-07-06 03:56:49 -07004327 if options.cq_dry_run and options.use_commit_queue:
4328 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4329
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004330 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4331 settings.GetIsGerrit()
4332
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004333 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004334 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004335
4336
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004337def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004338 print()
4339 print('Waiting for commit to be landed on %s...' % real_ref)
4340 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004341 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4342 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004343 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004344
4345 loop = 0
4346 while True:
4347 sys.stdout.write('fetching (%d)... \r' % loop)
4348 sys.stdout.flush()
4349 loop += 1
4350
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004351 if mirror:
4352 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004353 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4354 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4355 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4356 for commit in commits.splitlines():
4357 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004358 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004359 return commit
4360
4361 current_rev = to_rev
4362
4363
tandriibf429402016-09-14 07:09:12 -07004364def PushToGitPending(remote, pending_ref):
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004365 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4366
4367 Returns:
4368 (retcode of last operation, output log of last operation).
4369 """
4370 assert pending_ref.startswith('refs/'), pending_ref
4371 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4372 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4373 code = 0
4374 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004375 max_attempts = 3
4376 attempts_left = max_attempts
4377 while attempts_left:
4378 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004379 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004380 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004381
4382 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004383 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004384 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004385 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004386 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004387 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004388 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004389 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004390 continue
4391
4392 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004393 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004394 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004395 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004396 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004397 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4398 'the following files have merge conflicts:' % pending_ref)
4399 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4400 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004401 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004402 return code, out
4403
4404 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004405 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004406 code, out = RunGitWithCode(
4407 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4408 if code == 0:
4409 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004410 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004411 return code, out
4412
vapiera7fbd5a2016-06-16 09:17:49 -07004413 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004414 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004415 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004416 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004417 print('Fatal push error. Make sure your .netrc credentials and git '
4418 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004419 return code, out
4420
vapiera7fbd5a2016-06-16 09:17:49 -07004421 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004422 return code, out
4423
4424
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004425def IsFatalPushFailure(push_stdout):
4426 """True if retrying push won't help."""
4427 return '(prohibited by Gerrit)' in push_stdout
4428
4429
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004430@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004431def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004432 """DEPRECATED: Used to commit the current changelist via git-svn."""
4433 message = ('git-cl no longer supports committing to SVN repositories via '
4434 'git-svn. You probably want to use `git cl land` instead.')
4435 print(message)
4436 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004437
4438
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004439@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004440def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004441 """Commits the current changelist via git.
4442
4443 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4444 upstream and closes the issue automatically and atomically.
4445
4446 Otherwise (in case of Rietveld):
4447 Squashes branch into a single commit.
4448 Updates commit message with metadata (e.g. pointer to review).
4449 Pushes the code upstream.
4450 Updates review and closes.
4451 """
4452 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4453 help='bypass upload presubmit hook')
4454 parser.add_option('-m', dest='message',
4455 help="override review description")
4456 parser.add_option('-f', action='store_true', dest='force',
4457 help="force yes to questions (don't prompt)")
4458 parser.add_option('-c', dest='contributor',
4459 help="external contributor for patch (appended to " +
4460 "description and used as author for git). Should be " +
4461 "formatted as 'First Last <email@example.com>'")
4462 add_git_similarity(parser)
4463 auth.add_auth_options(parser)
4464 (options, args) = parser.parse_args(args)
4465 auth_config = auth.extract_auth_config_from_options(options)
4466
4467 cl = Changelist(auth_config=auth_config)
4468
4469 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4470 if cl.IsGerrit():
4471 if options.message:
4472 # This could be implemented, but it requires sending a new patch to
4473 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4474 # Besides, Gerrit has the ability to change the commit message on submit
4475 # automatically, thus there is no need to support this option (so far?).
4476 parser.error('-m MESSAGE option is not supported for Gerrit.')
4477 if options.contributor:
4478 parser.error(
4479 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4480 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4481 'the contributor\'s "name <email>". If you can\'t upload such a '
4482 'commit for review, contact your repository admin and request'
4483 '"Forge-Author" permission.')
4484 if not cl.GetIssue():
4485 DieWithError('You must upload the change first to Gerrit.\n'
4486 ' If you would rather have `git cl land` upload '
4487 'automatically for you, see http://crbug.com/642759')
4488 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4489 options.verbose)
4490
4491 current = cl.GetBranch()
4492 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4493 if remote == '.':
4494 print()
4495 print('Attempting to push branch %r into another local branch!' % current)
4496 print()
4497 print('Either reparent this branch on top of origin/master:')
4498 print(' git reparent-branch --root')
4499 print()
4500 print('OR run `git rebase-update` if you think the parent branch is ')
4501 print('already committed.')
4502 print()
4503 print(' Current parent: %r' % upstream_branch)
4504 return 1
4505
4506 if not args:
4507 # Default to merging against our best guess of the upstream branch.
4508 args = [cl.GetUpstreamBranch()]
4509
4510 if options.contributor:
4511 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
4512 print("Please provide contibutor as 'First Last <email@example.com>'")
4513 return 1
4514
4515 base_branch = args[0]
4516
4517 if git_common.is_dirty_git_tree('land'):
4518 return 1
4519
4520 # This rev-list syntax means "show all commits not in my branch that
4521 # are in base_branch".
4522 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4523 base_branch]).splitlines()
4524 if upstream_commits:
4525 print('Base branch "%s" has %d commits '
4526 'not in this branch.' % (base_branch, len(upstream_commits)))
4527 print('Run "git merge %s" before attempting to land.' % base_branch)
4528 return 1
4529
4530 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
4531 if not options.bypass_hooks:
4532 author = None
4533 if options.contributor:
4534 author = re.search(r'\<(.*)\>', options.contributor).group(1)
4535 hook_results = cl.RunHook(
4536 committing=True,
4537 may_prompt=not options.force,
4538 verbose=options.verbose,
4539 change=cl.GetChange(merge_base, author))
4540 if not hook_results.should_continue():
4541 return 1
4542
4543 # Check the tree status if the tree status URL is set.
4544 status = GetTreeStatus()
4545 if 'closed' == status:
4546 print('The tree is closed. Please wait for it to reopen. Use '
4547 '"git cl land --bypass-hooks" to commit on a closed tree.')
4548 return 1
4549 elif 'unknown' == status:
4550 print('Unable to determine tree status. Please verify manually and '
4551 'use "git cl land --bypass-hooks" to commit on a closed tree.')
4552 return 1
4553
4554 change_desc = ChangeDescription(options.message)
4555 if not change_desc.description and cl.GetIssue():
4556 change_desc = ChangeDescription(cl.GetDescription())
4557
4558 if not change_desc.description:
4559 if not cl.GetIssue() and options.bypass_hooks:
4560 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
4561 else:
4562 print('No description set.')
4563 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
4564 return 1
4565
4566 # Keep a separate copy for the commit message, because the commit message
4567 # contains the link to the Rietveld issue, while the Rietveld message contains
4568 # the commit viewvc url.
4569 if cl.GetIssue():
4570 change_desc.update_reviewers(cl.GetApprovingReviewers())
4571
4572 commit_desc = ChangeDescription(change_desc.description)
4573 if cl.GetIssue():
4574 # Xcode won't linkify this URL unless there is a non-whitespace character
4575 # after it. Add a period on a new line to circumvent this. Also add a space
4576 # before the period to make sure that Gitiles continues to correctly resolve
4577 # the URL.
4578 commit_desc.append_footer('Review-Url: %s .' % cl.GetIssueURL())
4579 if options.contributor:
4580 commit_desc.append_footer('Patch from %s.' % options.contributor)
4581
4582 print('Description:')
4583 print(commit_desc.description)
4584
4585 branches = [merge_base, cl.GetBranchRef()]
4586 if not options.force:
4587 print_stats(options.similarity, options.find_copies, branches)
4588
4589 # We want to squash all this branch's commits into one commit with the proper
4590 # description. We do this by doing a "reset --soft" to the base branch (which
4591 # keeps the working copy the same), then landing that.
4592 MERGE_BRANCH = 'git-cl-commit'
4593 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4594 # Delete the branches if they exist.
4595 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4596 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4597 result = RunGitWithCode(showref_cmd)
4598 if result[0] == 0:
4599 RunGit(['branch', '-D', branch])
4600
4601 # We might be in a directory that's present in this branch but not in the
4602 # trunk. Move up to the top of the tree so that git commands that expect a
4603 # valid CWD won't fail after we check out the merge branch.
4604 rel_base_path = settings.GetRelativeRoot()
4605 if rel_base_path:
4606 os.chdir(rel_base_path)
4607
4608 # Stuff our change into the merge branch.
4609 # We wrap in a try...finally block so if anything goes wrong,
4610 # we clean up the branches.
4611 retcode = -1
4612 pushed_to_pending = False
4613 pending_ref = None
4614 revision = None
4615 try:
4616 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
4617 RunGit(['reset', '--soft', merge_base])
4618 if options.contributor:
4619 RunGit(
4620 [
4621 'commit', '--author', options.contributor,
4622 '-m', commit_desc.description,
4623 ])
4624 else:
4625 RunGit(['commit', '-m', commit_desc.description])
4626
4627 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
4628 mirror = settings.GetGitMirror(remote)
4629 if mirror:
4630 pushurl = mirror.url
4631 git_numberer = _GitNumbererState.load(pushurl, branch)
4632 else:
4633 pushurl = remote # Usually, this is 'origin'.
4634 git_numberer = _GitNumbererState.load(
4635 RunGit(['config', 'remote.%s.url' % remote]).strip(), branch)
4636
4637 if git_numberer.should_add_git_number:
4638 # TODO(tandrii): run git fetch in a loop + autorebase when there there
4639 # is no pending ref to push to?
4640 logging.debug('Adding git number footers')
4641 parent_msg = RunGit(['show', '-s', '--format=%B', merge_base]).strip()
4642 commit_desc.update_with_git_number_footers(merge_base, parent_msg,
4643 branch)
4644 # Ensure timestamps are monotonically increasing.
4645 timestamp = max(1 + _get_committer_timestamp(merge_base),
4646 _get_committer_timestamp('HEAD'))
4647 _git_amend_head(commit_desc.description, timestamp)
4648 change_desc = ChangeDescription(commit_desc.description)
4649 # If gnumbd is sitll ON and we ultimately push to branch with
4650 # pending_prefix, gnumbd will modify footers we've just inserted with
4651 # 'Original-', which is annoying but still technically correct.
4652
4653 pending_prefix = git_numberer.pending_prefix
4654 if not pending_prefix or branch.startswith(pending_prefix):
4655 # If not using refs/pending/heads/* at all, or target ref is already set
4656 # to pending, then push to the target ref directly.
4657 # NB(tandrii): I think branch.startswith(pending_prefix) never happens
4658 # in practise. I really tried to create a new branch tracking
4659 # refs/pending/heads/master directly and git cl land failed long before
4660 # reaching this. Disagree? Comment on http://crbug.com/642493.
4661 if pending_prefix:
4662 print('\n\nYOU GOT A CHANCE TO WIN A FREE GIFT!\n\n'
4663 'Grab your .git/config, add instructions how to reproduce '
4664 'this, and post it to http://crbug.com/642493.\n'
4665 'The first reporter gets a free "Black Swan" book from '
4666 'tandrii@\n\n')
4667 retcode, output = RunGitWithCode(
4668 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
4669 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
4670 else:
4671 # Cherry-pick the change on top of pending ref and then push it.
4672 assert branch.startswith('refs/'), branch
4673 assert pending_prefix[-1] == '/', pending_prefix
4674 pending_ref = pending_prefix + branch[len('refs/'):]
4675 retcode, output = PushToGitPending(pushurl, pending_ref)
4676 pushed_to_pending = (retcode == 0)
4677
4678 if retcode == 0:
4679 revision = RunGit(['rev-parse', 'HEAD']).strip()
4680 logging.debug(output)
4681 except: # pylint: disable=bare-except
4682 if _IS_BEING_TESTED:
4683 logging.exception('this is likely your ACTUAL cause of test failure.\n'
4684 + '-' * 30 + '8<' + '-' * 30)
4685 logging.error('\n' + '-' * 30 + '8<' + '-' * 30 + '\n\n\n')
4686 raise
4687 finally:
4688 # And then swap back to the original branch and clean up.
4689 RunGit(['checkout', '-q', cl.GetBranch()])
4690 RunGit(['branch', '-D', MERGE_BRANCH])
4691
4692 if not revision:
4693 print('Failed to push. If this persists, please file a bug.')
4694 return 1
4695
4696 killed = False
4697 if pushed_to_pending:
4698 try:
4699 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4700 # We set pushed_to_pending to False, since it made it all the way to the
4701 # real ref.
4702 pushed_to_pending = False
4703 except KeyboardInterrupt:
4704 killed = True
4705
4706 if cl.GetIssue():
4707 to_pending = ' to pending queue' if pushed_to_pending else ''
4708 viewvc_url = settings.GetViewVCUrl()
4709 if not to_pending:
4710 if viewvc_url and revision:
4711 change_desc.append_footer(
4712 'Committed: %s%s' % (viewvc_url, revision))
4713 elif revision:
4714 change_desc.append_footer('Committed: %s' % (revision,))
4715 print('Closing issue '
4716 '(you may be prompted for your codereview password)...')
4717 cl.UpdateDescription(change_desc.description)
4718 cl.CloseIssue()
4719 props = cl.GetIssueProperties()
4720 patch_num = len(props['patchsets'])
4721 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
4722 patch_num, props['patchsets'][-1], to_pending, revision)
4723 if options.bypass_hooks:
4724 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4725 else:
4726 comment += ' (presubmit successful).'
4727 cl.RpcServer().add_comment(cl.GetIssue(), comment)
4728
4729 if pushed_to_pending:
4730 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
4731 print('The commit is in the pending queue (%s).' % pending_ref)
4732 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4733 'footer.' % branch)
4734
4735 if os.path.isfile(POSTUPSTREAM_HOOK):
4736 RunCommand([POSTUPSTREAM_HOOK, merge_base], error_ok=True)
4737
4738 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004739
4740
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004741@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004742def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004743 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004744 parser.add_option('-b', dest='newbranch',
4745 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004746 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004747 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004748 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4749 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004750 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004751 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004752 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004753 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004754 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004755 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004756
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004757
4758 group = optparse.OptionGroup(
4759 parser,
4760 'Options for continuing work on the current issue uploaded from a '
4761 'different clone (e.g. different machine). Must be used independently '
4762 'from the other options. No issue number should be specified, and the '
4763 'branch must have an issue number associated with it')
4764 group.add_option('--reapply', action='store_true', dest='reapply',
4765 help='Reset the branch and reapply the issue.\n'
4766 'CAUTION: This will undo any local changes in this '
4767 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004768
4769 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004770 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004771 parser.add_option_group(group)
4772
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004773 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004774 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004775 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004776 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004777 auth_config = auth.extract_auth_config_from_options(options)
4778
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004779
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004780 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004781 if options.newbranch:
4782 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004783 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004784 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004785
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004786 cl = Changelist(auth_config=auth_config,
4787 codereview=options.forced_codereview)
4788 if not cl.GetIssue():
4789 parser.error('current branch must have an associated issue')
4790
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004791 upstream = cl.GetUpstreamBranch()
4792 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004793 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004794
4795 RunGit(['reset', '--hard', upstream])
4796 if options.pull:
4797 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004798
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004799 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4800 options.directory)
4801
4802 if len(args) != 1 or not args[0]:
4803 parser.error('Must specify issue number or url')
4804
4805 # We don't want uncommitted changes mixed up with the patch.
4806 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004807 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004808
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004809 if options.newbranch:
4810 if options.force:
4811 RunGit(['branch', '-D', options.newbranch],
4812 stderr=subprocess2.PIPE, error_ok=True)
4813 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004814 elif not GetCurrentBranch():
4815 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004816
4817 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4818
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004819 if cl.IsGerrit():
4820 if options.reject:
4821 parser.error('--reject is not supported with Gerrit codereview.')
4822 if options.nocommit:
4823 parser.error('--nocommit is not supported with Gerrit codereview.')
4824 if options.directory:
4825 parser.error('--directory is not supported with Gerrit codereview.')
4826
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004827 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004828 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004829
4830
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004831def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004832 """Fetches the tree status and returns either 'open', 'closed',
4833 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004834 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004835 if url:
4836 status = urllib2.urlopen(url).read().lower()
4837 if status.find('closed') != -1 or status == '0':
4838 return 'closed'
4839 elif status.find('open') != -1 or status == '1':
4840 return 'open'
4841 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004842 return 'unset'
4843
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004844
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004845def GetTreeStatusReason():
4846 """Fetches the tree status from a json url and returns the message
4847 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004848 url = settings.GetTreeStatusUrl()
4849 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004850 connection = urllib2.urlopen(json_url)
4851 status = json.loads(connection.read())
4852 connection.close()
4853 return status['message']
4854
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004855
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004856def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004857 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004858 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004859 status = GetTreeStatus()
4860 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004861 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004862 return 2
4863
vapiera7fbd5a2016-06-16 09:17:49 -07004864 print('The tree is %s' % status)
4865 print()
4866 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004867 if status != 'open':
4868 return 1
4869 return 0
4870
4871
maruel@chromium.org15192402012-09-06 12:38:29 +00004872def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004873 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004874 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004875 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004876 '-b', '--bot', action='append',
4877 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4878 'times to specify multiple builders. ex: '
4879 '"-b win_rel -b win_layout". See '
4880 'the try server waterfall for the builders name and the tests '
4881 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004882 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004883 '-B', '--bucket', default='',
4884 help=('Buildbucket bucket to send the try requests.'))
4885 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004886 '-m', '--master', default='',
4887 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004888 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004889 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004890 help='Revision to use for the try job; default: the revision will '
4891 'be determined by the try recipe that builder runs, which usually '
4892 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004893 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004894 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004895 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004896 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004897 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004898 '--project',
4899 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004900 'in recipe to determine to which repository or directory to '
4901 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004902 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004903 '-p', '--property', dest='properties', action='append', default=[],
4904 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004905 'key2=value2 etc. The value will be treated as '
4906 'json if decodable, or as string otherwise. '
4907 'NOTE: using this may make your try job not usable for CQ, '
4908 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004909 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004910 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4911 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004912 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004913 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004914 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004915 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004916
machenbach@chromium.org45453142015-09-15 08:45:22 +00004917 # Make sure that all properties are prop=value pairs.
4918 bad_params = [x for x in options.properties if '=' not in x]
4919 if bad_params:
4920 parser.error('Got properties with missing "=": %s' % bad_params)
4921
maruel@chromium.org15192402012-09-06 12:38:29 +00004922 if args:
4923 parser.error('Unknown arguments: %s' % args)
4924
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004925 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004926 if not cl.GetIssue():
4927 parser.error('Need to upload first')
4928
tandriie113dfd2016-10-11 10:20:12 -07004929 error_message = cl.CannotTriggerTryJobReason()
4930 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004931 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004932
borenet6c0efe62016-10-19 08:13:29 -07004933 if options.bucket and options.master:
4934 parser.error('Only one of --bucket and --master may be used.')
4935
qyearsley1fdfcb62016-10-24 13:22:03 -07004936 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004937
qyearsleydd49f942016-10-28 11:57:22 -07004938 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4939 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004940 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004941 if options.verbose:
4942 print('git cl try with no bots now defaults to CQ Dry Run.')
4943 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004944
borenet6c0efe62016-10-19 08:13:29 -07004945 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004946 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004947 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004948 'of bot requires an initial job from a parent (usually a builder). '
4949 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004950 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004951 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004952
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004953 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004954 # TODO(tandrii): Checking local patchset against remote patchset is only
4955 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4956 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07004957 print('Warning: Codereview server has newer patchsets (%s) than most '
4958 'recent upload from local checkout (%s). Did a previous upload '
4959 'fail?\n'
4960 'By default, git cl try uses the latest patchset from '
4961 'codereview, continuing to use patchset %s.\n' %
4962 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07004963
tandrii568043b2016-10-11 07:49:18 -07004964 try:
borenet6c0efe62016-10-19 08:13:29 -07004965 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
4966 patchset)
tandrii568043b2016-10-11 07:49:18 -07004967 except BuildbucketResponseException as ex:
4968 print('ERROR: %s' % ex)
4969 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004970 return 0
4971
4972
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004973def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004974 """Prints info about try jobs associated with current CL."""
4975 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004976 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004977 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004978 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004979 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004980 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004981 '--color', action='store_true', default=setup_color.IS_TTY,
4982 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004983 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004984 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4985 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004986 group.add_option(
4987 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004988 parser.add_option_group(group)
4989 auth.add_auth_options(parser)
4990 options, args = parser.parse_args(args)
4991 if args:
4992 parser.error('Unrecognized args: %s' % ' '.join(args))
4993
4994 auth_config = auth.extract_auth_config_from_options(options)
4995 cl = Changelist(auth_config=auth_config)
4996 if not cl.GetIssue():
4997 parser.error('Need to upload first')
4998
tandrii221ab252016-10-06 08:12:04 -07004999 patchset = options.patchset
5000 if not patchset:
5001 patchset = cl.GetMostRecentPatchset()
5002 if not patchset:
5003 parser.error('Codereview doesn\'t know about issue %s. '
5004 'No access to issue or wrong issue number?\n'
5005 'Either upload first, or pass --patchset explicitely' %
5006 cl.GetIssue())
5007
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005008 # TODO(tandrii): Checking local patchset against remote patchset is only
5009 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5010 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07005011 print('Warning: Codereview server has newer patchsets (%s) than most '
5012 'recent upload from local checkout (%s). Did a previous upload '
5013 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07005014 'By default, git cl try-results uses the latest patchset from '
5015 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07005016 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005017 try:
tandrii221ab252016-10-06 08:12:04 -07005018 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005019 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005020 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005021 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005022 if options.json:
5023 write_try_results_json(options.json, jobs)
5024 else:
5025 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005026 return 0
5027
5028
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005029@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005030def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005031 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005032 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005033 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005034 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005035
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005036 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005037 if args:
5038 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005039 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005040 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005041 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005042 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005043
5044 # Clear configured merge-base, if there is one.
5045 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005046 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005047 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005048 return 0
5049
5050
thestig@chromium.org00858c82013-12-02 23:08:03 +00005051def CMDweb(parser, args):
5052 """Opens the current CL in the web browser."""
5053 _, args = parser.parse_args(args)
5054 if args:
5055 parser.error('Unrecognized args: %s' % ' '.join(args))
5056
5057 issue_url = Changelist().GetIssueURL()
5058 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005059 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005060 return 1
5061
5062 webbrowser.open(issue_url)
5063 return 0
5064
5065
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005066def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005067 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005068 parser.add_option('-d', '--dry-run', action='store_true',
5069 help='trigger in dry run mode')
5070 parser.add_option('-c', '--clear', action='store_true',
5071 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005072 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005073 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005074 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005075 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005076 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005077 if args:
5078 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005079 if options.dry_run and options.clear:
5080 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5081
iannuccie53c9352016-08-17 14:40:40 -07005082 cl = Changelist(auth_config=auth_config, issue=options.issue,
5083 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005084 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005085 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005086 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07005087 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005088 state = _CQState.DRY_RUN
5089 else:
5090 state = _CQState.COMMIT
5091 if not cl.GetIssue():
5092 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005093 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005094 return 0
5095
5096
groby@chromium.org411034a2013-02-26 15:12:01 +00005097def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005098 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005099 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005100 auth.add_auth_options(parser)
5101 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005102 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005103 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005104 if args:
5105 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005106 cl = Changelist(auth_config=auth_config, issue=options.issue,
5107 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005108 # Ensure there actually is an issue to close.
5109 cl.GetDescription()
5110 cl.CloseIssue()
5111 return 0
5112
5113
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005114def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005115 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005116 parser.add_option(
5117 '--stat',
5118 action='store_true',
5119 dest='stat',
5120 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005121 auth.add_auth_options(parser)
5122 options, args = parser.parse_args(args)
5123 auth_config = auth.extract_auth_config_from_options(options)
5124 if args:
5125 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005126
5127 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005128 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005129 # Staged changes would be committed along with the patch from last
5130 # upload, hence counted toward the "last upload" side in the final
5131 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005132 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005133 return 1
5134
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005135 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005136 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005137 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005138 if not issue:
5139 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005140 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005141 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005142
5143 # Create a new branch based on the merge-base
5144 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005145 # Clear cached branch in cl object, to avoid overwriting original CL branch
5146 # properties.
5147 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005148 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005149 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005150 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005151 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005152 return rtn
5153
wychen@chromium.org06928532015-02-03 02:11:29 +00005154 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005155 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005156 cmd = ['git', 'diff']
5157 if options.stat:
5158 cmd.append('--stat')
5159 cmd.extend([TMP_BRANCH, branch, '--'])
5160 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005161 finally:
5162 RunGit(['checkout', '-q', branch])
5163 RunGit(['branch', '-D', TMP_BRANCH])
5164
5165 return 0
5166
5167
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005168def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005169 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005170 parser.add_option(
5171 '--no-color',
5172 action='store_true',
5173 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005174 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005175 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005176 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005177
5178 author = RunGit(['config', 'user.email']).strip() or None
5179
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005180 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005181
5182 if args:
5183 if len(args) > 1:
5184 parser.error('Unknown args')
5185 base_branch = args[0]
5186 else:
5187 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005188 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005189
5190 change = cl.GetChange(base_branch, None)
5191 return owners_finder.OwnersFinder(
5192 [f.LocalPath() for f in
5193 cl.GetChange(base_branch, None).AffectedFiles()],
5194 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005195 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005196 disable_color=options.no_color).run()
5197
5198
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005199def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005200 """Generates a diff command."""
5201 # Generate diff for the current branch's changes.
5202 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
5203 upstream_commit, '--' ]
5204
5205 if args:
5206 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005207 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005208 diff_cmd.append(arg)
5209 else:
5210 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005211
5212 return diff_cmd
5213
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005214def MatchingFileType(file_name, extensions):
5215 """Returns true if the file name ends with one of the given extensions."""
5216 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005217
enne@chromium.org555cfe42014-01-29 18:21:39 +00005218@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005219def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005220 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005221 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005222 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005223 parser.add_option('--full', action='store_true',
5224 help='Reformat the full content of all touched files')
5225 parser.add_option('--dry-run', action='store_true',
5226 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005227 parser.add_option('--python', action='store_true',
5228 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005229 parser.add_option('--js', action='store_true',
5230 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005231 parser.add_option('--diff', action='store_true',
5232 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005233 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005234
Daniel Chengc55eecf2016-12-30 03:11:02 -08005235 # Normalize any remaining args against the current path, so paths relative to
5236 # the current directory are still resolved as expected.
5237 args = [os.path.join(os.getcwd(), arg) for arg in args]
5238
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005239 # git diff generates paths against the root of the repository. Change
5240 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005241 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005242 if rel_base_path:
5243 os.chdir(rel_base_path)
5244
digit@chromium.org29e47272013-05-17 17:01:46 +00005245 # Grab the merge-base commit, i.e. the upstream commit of the current
5246 # branch when it was created or the last time it was rebased. This is
5247 # to cover the case where the user may have called "git fetch origin",
5248 # moving the origin branch to a newer commit, but hasn't rebased yet.
5249 upstream_commit = None
5250 cl = Changelist()
5251 upstream_branch = cl.GetUpstreamBranch()
5252 if upstream_branch:
5253 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5254 upstream_commit = upstream_commit.strip()
5255
5256 if not upstream_commit:
5257 DieWithError('Could not find base commit for this branch. '
5258 'Are you in detached state?')
5259
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005260 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5261 diff_output = RunGit(changed_files_cmd)
5262 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005263 # Filter out files deleted by this CL
5264 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005265
Christopher Lamc5ba6922017-01-24 11:19:14 +11005266 if opts.js:
5267 CLANG_EXTS.append('.js')
5268
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005269 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5270 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5271 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005272 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005273
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005274 top_dir = os.path.normpath(
5275 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5276
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005277 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5278 # formatted. This is used to block during the presubmit.
5279 return_value = 0
5280
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005281 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005282 # Locate the clang-format binary in the checkout
5283 try:
5284 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005285 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005286 DieWithError(e)
5287
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005288 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005289 cmd = [clang_format_tool]
5290 if not opts.dry_run and not opts.diff:
5291 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005292 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005293 if opts.diff:
5294 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005295 else:
5296 env = os.environ.copy()
5297 env['PATH'] = str(os.path.dirname(clang_format_tool))
5298 try:
5299 script = clang_format.FindClangFormatScriptInChromiumTree(
5300 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005301 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005302 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005303
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005304 cmd = [sys.executable, script, '-p0']
5305 if not opts.dry_run and not opts.diff:
5306 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005307
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005308 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5309 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005310
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005311 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5312 if opts.diff:
5313 sys.stdout.write(stdout)
5314 if opts.dry_run and len(stdout) > 0:
5315 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005316
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005317 # Similar code to above, but using yapf on .py files rather than clang-format
5318 # on C/C++ files
5319 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005320 yapf_tool = gclient_utils.FindExecutable('yapf')
5321 if yapf_tool is None:
5322 DieWithError('yapf not found in PATH')
5323
5324 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005325 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005326 cmd = [yapf_tool]
5327 if not opts.dry_run and not opts.diff:
5328 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005329 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005330 if opts.diff:
5331 sys.stdout.write(stdout)
5332 else:
5333 # TODO(sbc): yapf --lines mode still has some issues.
5334 # https://github.com/google/yapf/issues/154
5335 DieWithError('--python currently only works with --full')
5336
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005337 # Dart's formatter does not have the nice property of only operating on
5338 # modified chunks, so hard code full.
5339 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005340 try:
5341 command = [dart_format.FindDartFmtToolInChromiumTree()]
5342 if not opts.dry_run and not opts.diff:
5343 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005344 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005345
ppi@chromium.org6593d932016-03-03 15:41:15 +00005346 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005347 if opts.dry_run and stdout:
5348 return_value = 2
5349 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005350 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5351 'found in this checkout. Files in other languages are still '
5352 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005353
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005354 # Format GN build files. Always run on full build files for canonical form.
5355 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005356 cmd = ['gn', 'format' ]
5357 if opts.dry_run or opts.diff:
5358 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005359 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005360 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5361 shell=sys.platform == 'win32',
5362 cwd=top_dir)
5363 if opts.dry_run and gn_ret == 2:
5364 return_value = 2 # Not formatted.
5365 elif opts.diff and gn_ret == 2:
5366 # TODO this should compute and print the actual diff.
5367 print("This change has GN build file diff for " + gn_diff_file)
5368 elif gn_ret != 0:
5369 # For non-dry run cases (and non-2 return values for dry-run), a
5370 # nonzero error code indicates a failure, probably because the file
5371 # doesn't parse.
5372 DieWithError("gn format failed on " + gn_diff_file +
5373 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005374
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005375 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005376
5377
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005378@subcommand.usage('<codereview url or issue id>')
5379def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005380 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005381 _, args = parser.parse_args(args)
5382
5383 if len(args) != 1:
5384 parser.print_help()
5385 return 1
5386
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005387 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005388 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005389 parser.print_help()
5390 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005391 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005392
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005393 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005394 output = RunGit(['config', '--local', '--get-regexp',
5395 r'branch\..*\.%s' % issueprefix],
5396 error_ok=True)
5397 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005398 if issue == target_issue:
5399 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005400
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005401 branches = []
5402 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005403 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005404 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005405 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005406 return 1
5407 if len(branches) == 1:
5408 RunGit(['checkout', branches[0]])
5409 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005410 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005411 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005412 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005413 which = raw_input('Choose by index: ')
5414 try:
5415 RunGit(['checkout', branches[int(which)]])
5416 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005417 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005418 return 1
5419
5420 return 0
5421
5422
maruel@chromium.org29404b52014-09-08 22:58:00 +00005423def CMDlol(parser, args):
5424 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005425 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005426 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5427 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5428 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005429 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005430 return 0
5431
5432
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005433class OptionParser(optparse.OptionParser):
5434 """Creates the option parse and add --verbose support."""
5435 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005436 optparse.OptionParser.__init__(
5437 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005438 self.add_option(
5439 '-v', '--verbose', action='count', default=0,
5440 help='Use 2 times for more debugging info')
5441
5442 def parse_args(self, args=None, values=None):
5443 options, args = optparse.OptionParser.parse_args(self, args, values)
5444 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005445 logging.basicConfig(
5446 level=levels[min(options.verbose, len(levels) - 1)],
5447 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5448 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005449 return options, args
5450
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005451
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005452def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005453 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005454 print('\nYour python version %s is unsupported, please upgrade.\n' %
5455 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005456 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005457
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005458 # Reload settings.
5459 global settings
5460 settings = Settings()
5461
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005462 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005463 dispatcher = subcommand.CommandDispatcher(__name__)
5464 try:
5465 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005466 except auth.AuthenticationError as e:
5467 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005468 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005469 if e.code != 500:
5470 raise
5471 DieWithError(
5472 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5473 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005474 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005475
5476
5477if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005478 # These affect sys.stdout so do it outside of main() to simplify mocks in
5479 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005480 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005481 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005482 try:
5483 sys.exit(main(sys.argv[1:]))
5484 except KeyboardInterrupt:
5485 sys.stderr.write('interrupted\n')
5486 sys.exit(1)