blob: 912a519631ab6c64cacb0110ea036b2f0b631ee5 [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 Shyshkalovf3a20ae2017-01-24 21:23:57 +010016import contextlib
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +010017import fnmatch
sheyang@google.com6ebaf782015-05-12 19:17:54 +000018import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000019import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000021import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import optparse
23import os
24import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000025import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000026import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000027import textwrap
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
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000046import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000047import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000048import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000049import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000050import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000051import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000052import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000053import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000054import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000055import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000056import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000057import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000059import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000061import watchlists
62
tandrii7400cf02016-06-21 08:48:07 -070063__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000064
tandrii9d2c7a32016-06-22 03:42:45 -070065COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070066DEFAULT_SERVER = 'https://codereview.chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080067POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +000069REFS_THAT_ALIAS_TO_OTHER_REFS = {
70 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
71 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
72}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000073
thestig@chromium.org44202a22014-03-11 19:22:18 +000074# Valid extensions for files we want to lint.
75DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
76DEFAULT_LINT_IGNORE_REGEX = r"$^"
77
borenet6c0efe62016-10-19 08:13:29 -070078# Buildbucket master name prefix.
79MASTER_PREFIX = 'master.'
80
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000081# Shortcut since it quickly becomes redundant.
82Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000083
maruel@chromium.orgddd59412011-11-30 14:20:38 +000084# Initialized in main()
85settings = None
86
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010087# Used by tests/git_cl_test.py to add extra logging.
88# Inside the weirdly failing test, add this:
89# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
90# And scroll up to see the strack trace printed.
91_IS_BEING_TESTED = False
92
maruel@chromium.orgddd59412011-11-30 14:20:38 +000093
Christopher Lamf732cd52017-01-24 12:40:11 +110094def DieWithError(message, change_desc=None):
95 if change_desc:
96 SaveDescriptionBackup(change_desc)
97
vapiera7fbd5a2016-06-16 09:17:49 -070098 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000099 sys.exit(1)
100
101
Christopher Lamf732cd52017-01-24 12:40:11 +1100102def SaveDescriptionBackup(change_desc):
103 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
104 print('\nError after CL description prompt -- saving description to %s\n' %
105 backup_path)
106 backup_file = open(backup_path, 'w')
107 backup_file.write(change_desc.description)
108 backup_file.close()
109
110
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000111def GetNoGitPagerEnv():
112 env = os.environ.copy()
113 # 'cat' is a magical git string that disables pagers on all platforms.
114 env['GIT_PAGER'] = 'cat'
115 return env
116
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000117
bsep@chromium.org627d9002016-04-29 00:00:52 +0000118def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000119 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000120 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000121 except subprocess2.CalledProcessError as e:
122 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000123 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000124 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000125 'Command "%s" failed.\n%s' % (
126 ' '.join(args), error_message or e.stdout or ''))
127 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000128
129
130def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000131 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000132 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000133
134
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000135def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000136 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700137 if suppress_stderr:
138 stderr = subprocess2.VOID
139 else:
140 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000141 try:
tandrii5d48c322016-08-18 16:19:37 -0700142 (out, _), code = subprocess2.communicate(['git'] + args,
143 env=GetNoGitPagerEnv(),
144 stdout=subprocess2.PIPE,
145 stderr=stderr)
146 return code, out
147 except subprocess2.CalledProcessError as e:
148 logging.debug('Failed running %s', args)
149 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000150
151
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000152def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000153 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000154 return RunGitWithCode(args, suppress_stderr=True)[1]
155
156
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000157def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000158 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000159 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000160 return (version.startswith(prefix) and
161 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000162
163
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000164def BranchExists(branch):
165 """Return True if specified branch exists."""
166 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
167 suppress_stderr=True)
168 return not code
169
170
tandrii2a16b952016-10-19 07:09:44 -0700171def time_sleep(seconds):
172 # Use this so that it can be mocked in tests without interfering with python
173 # system machinery.
174 import time # Local import to discourage others from importing time globally.
175 return time.sleep(seconds)
176
177
maruel@chromium.org90541732011-04-01 17:54:18 +0000178def ask_for_data(prompt):
179 try:
180 return raw_input(prompt)
181 except KeyboardInterrupt:
182 # Hide the exception.
183 sys.exit(1)
184
185
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100186def confirm_or_exit(prefix='', action='confirm'):
187 """Asks user to press enter to continue or press Ctrl+C to abort."""
188 if not prefix or prefix.endswith('\n'):
189 mid = 'Press'
190 elif prefix.endswith('.'):
191 mid = ' Press'
192 elif prefix.endswith(' '):
193 mid = 'press'
194 else:
195 mid = ' press'
196 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
197
198
199def ask_for_explicit_yes(prompt):
200 """Returns whether user typed 'y' or 'yes' to confirm the given prompt"""
201 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
202 while True:
203 if 'yes'.startswith(result):
204 return True
205 if 'no'.startswith(result):
206 return False
207 result = ask_for_data('Please, type yes or no: ').lower()
208
209
tandrii5d48c322016-08-18 16:19:37 -0700210def _git_branch_config_key(branch, key):
211 """Helper method to return Git config key for a branch."""
212 assert branch, 'branch name is required to set git config for it'
213 return 'branch.%s.%s' % (branch, key)
214
215
216def _git_get_branch_config_value(key, default=None, value_type=str,
217 branch=False):
218 """Returns git config value of given or current branch if any.
219
220 Returns default in all other cases.
221 """
222 assert value_type in (int, str, bool)
223 if branch is False: # Distinguishing default arg value from None.
224 branch = GetCurrentBranch()
225
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000226 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700227 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000228
tandrii5d48c322016-08-18 16:19:37 -0700229 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700230 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700231 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700232 # git config also has --int, but apparently git config suffers from integer
233 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700234 args.append(_git_branch_config_key(branch, key))
235 code, out = RunGitWithCode(args)
236 if code == 0:
237 value = out.strip()
238 if value_type == int:
239 return int(value)
240 if value_type == bool:
241 return bool(value.lower() == 'true')
242 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000243 return default
244
245
tandrii5d48c322016-08-18 16:19:37 -0700246def _git_set_branch_config_value(key, value, branch=None, **kwargs):
247 """Sets the value or unsets if it's None of a git branch config.
248
249 Valid, though not necessarily existing, branch must be provided,
250 otherwise currently checked out branch is used.
251 """
252 if not branch:
253 branch = GetCurrentBranch()
254 assert branch, 'a branch name OR currently checked out branch is required'
255 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700256 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700257 if value is None:
258 args.append('--unset')
259 elif isinstance(value, bool):
260 args.append('--bool')
261 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700262 else:
tandrii33a46ff2016-08-23 05:53:40 -0700263 # git config also has --int, but apparently git config suffers from integer
264 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700265 value = str(value)
266 args.append(_git_branch_config_key(branch, key))
267 if value is not None:
268 args.append(value)
269 RunGit(args, **kwargs)
270
271
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100272def _get_committer_timestamp(commit):
273 """Returns unix timestamp as integer of a committer in a commit.
274
275 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
276 """
277 # Git also stores timezone offset, but it only affects visual display,
278 # actual point in time is defined by this timestamp only.
279 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
280
281
282def _git_amend_head(message, committer_timestamp):
283 """Amends commit with new message and desired committer_timestamp.
284
285 Sets committer timezone to UTC.
286 """
287 env = os.environ.copy()
288 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
289 return RunGit(['commit', '--amend', '-m', message], env=env)
290
291
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000292def add_git_similarity(parser):
293 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700294 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000295 help='Sets the percentage that a pair of files need to match in order to'
296 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000297 parser.add_option(
298 '--find-copies', action='store_true',
299 help='Allows git to look for copies.')
300 parser.add_option(
301 '--no-find-copies', action='store_false', dest='find_copies',
302 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000303
304 old_parser_args = parser.parse_args
305 def Parse(args):
306 options, args = old_parser_args(args)
307
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000308 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700309 options.similarity = _git_get_branch_config_value(
310 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000311 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000312 print('Note: Saving similarity of %d%% in git config.'
313 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700314 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000315
iannucci@chromium.org79540052012-10-19 23:15:26 +0000316 options.similarity = max(0, min(options.similarity, 100))
317
318 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700319 options.find_copies = _git_get_branch_config_value(
320 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000321 else:
tandrii5d48c322016-08-18 16:19:37 -0700322 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000323
324 print('Using %d%% similarity for rename/copy detection. '
325 'Override with --similarity.' % options.similarity)
326
327 return options, args
328 parser.parse_args = Parse
329
330
machenbach@chromium.org45453142015-09-15 08:45:22 +0000331def _get_properties_from_options(options):
332 properties = dict(x.split('=', 1) for x in options.properties)
333 for key, val in properties.iteritems():
334 try:
335 properties[key] = json.loads(val)
336 except ValueError:
337 pass # If a value couldn't be evaluated, treat it as a string.
338 return properties
339
340
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000341def _prefix_master(master):
342 """Convert user-specified master name to full master name.
343
344 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
345 name, while the developers always use shortened master name
346 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
347 function does the conversion for buildbucket migration.
348 """
borenet6c0efe62016-10-19 08:13:29 -0700349 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000350 return master
borenet6c0efe62016-10-19 08:13:29 -0700351 return '%s%s' % (MASTER_PREFIX, master)
352
353
354def _unprefix_master(bucket):
355 """Convert bucket name to shortened master name.
356
357 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
358 name, while the developers always use shortened master name
359 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
360 function does the conversion for buildbucket migration.
361 """
362 if bucket.startswith(MASTER_PREFIX):
363 return bucket[len(MASTER_PREFIX):]
364 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000365
366
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000367def _buildbucket_retry(operation_name, http, *args, **kwargs):
368 """Retries requests to buildbucket service and returns parsed json content."""
369 try_count = 0
370 while True:
371 response, content = http.request(*args, **kwargs)
372 try:
373 content_json = json.loads(content)
374 except ValueError:
375 content_json = None
376
377 # Buildbucket could return an error even if status==200.
378 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000379 error = content_json.get('error')
380 if error.get('code') == 403:
381 raise BuildbucketResponseException(
382 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000383 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000384 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000385 raise BuildbucketResponseException(msg)
386
387 if response.status == 200:
388 if not content_json:
389 raise BuildbucketResponseException(
390 'Buildbucket returns invalid json content: %s.\n'
391 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
392 content)
393 return content_json
394 if response.status < 500 or try_count >= 2:
395 raise httplib2.HttpLib2Error(content)
396
397 # status >= 500 means transient failures.
398 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700399 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000400 try_count += 1
401 assert False, 'unreachable'
402
403
qyearsley1fdfcb62016-10-24 13:22:03 -0700404def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700405 """Returns a dict mapping bucket names to builders and tests,
406 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700407 """
qyearsleydd49f942016-10-28 11:57:22 -0700408 # If no bots are listed, we try to get a set of builders and tests based
409 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700410 if not options.bot:
411 change = changelist.GetChange(
412 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700413 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700414 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700415 change=change,
416 changed_files=change.LocalPaths(),
417 repository_root=settings.GetRoot(),
418 default_presubmit=None,
419 project=None,
420 verbose=options.verbose,
421 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700422 if masters is None:
423 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100424 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700425
qyearsley1fdfcb62016-10-24 13:22:03 -0700426 if options.bucket:
427 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700428 if options.master:
429 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700430
qyearsleydd49f942016-10-28 11:57:22 -0700431 # If bots are listed but no master or bucket, then we need to find out
432 # the corresponding master for each bot.
433 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
434 if error_message:
435 option_parser.error(
436 'Tryserver master cannot be found because: %s\n'
437 'Please manually specify the tryserver master, e.g. '
438 '"-m tryserver.chromium.linux".' % error_message)
439 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700440
441
qyearsley123a4682016-10-26 09:12:17 -0700442def _get_bucket_map_for_builders(builders):
443 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700444 map_url = 'https://builders-map.appspot.com/'
445 try:
qyearsley123a4682016-10-26 09:12:17 -0700446 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700447 except urllib2.URLError as e:
448 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
449 (map_url, e))
450 except ValueError as e:
451 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700452 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700453 return None, 'Failed to build master map.'
454
qyearsley123a4682016-10-26 09:12:17 -0700455 bucket_map = {}
456 for builder in builders:
qyearsley123a4682016-10-26 09:12:17 -0700457 masters = builders_map.get(builder, [])
458 if not masters:
qyearsley1fdfcb62016-10-24 13:22:03 -0700459 return None, ('No matching master for builder %s.' % builder)
qyearsley123a4682016-10-26 09:12:17 -0700460 if len(masters) > 1:
qyearsley1fdfcb62016-10-24 13:22:03 -0700461 return None, ('The builder name %s exists in multiple masters %s.' %
qyearsley123a4682016-10-26 09:12:17 -0700462 (builder, masters))
463 bucket = _prefix_master(masters[0])
464 bucket_map.setdefault(bucket, {})[builder] = []
465
466 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700467
468
borenet6c0efe62016-10-19 08:13:29 -0700469def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700470 category='git_cl_try', patchset=None):
qyearsley1fdfcb62016-10-24 13:22:03 -0700471 """Sends a request to Buildbucket to trigger try jobs for a changelist.
472
473 Args:
474 auth_config: AuthConfig for Rietveld.
475 changelist: Changelist that the try jobs are associated with.
476 buckets: A nested dict mapping bucket names to builders to tests.
477 options: Command-line options.
478 """
tandriide281ae2016-10-12 06:02:30 -0700479 assert changelist.GetIssue(), 'CL must be uploaded first'
480 codereview_url = changelist.GetCodereviewServer()
481 assert codereview_url, 'CL must be uploaded first'
482 patchset = patchset or changelist.GetMostRecentPatchset()
483 assert patchset, 'CL must be uploaded first'
484
485 codereview_host = urlparse.urlparse(codereview_url).hostname
486 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000487 http = authenticator.authorize(httplib2.Http())
488 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700489
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000490 buildbucket_put_url = (
491 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000492 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700493 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
494 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
495 hostname=codereview_host,
496 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000497 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700498
499 shared_parameters_properties = changelist.GetTryjobProperties(patchset)
500 shared_parameters_properties['category'] = category
501 if options.clobber:
502 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700503 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700504 if extra_properties:
505 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000506
507 batch_req_body = {'builds': []}
508 print_text = []
509 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700510 for bucket, builders_and_tests in sorted(buckets.iteritems()):
511 print_text.append('Bucket: %s' % bucket)
512 master = None
513 if bucket.startswith(MASTER_PREFIX):
514 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000515 for builder, tests in sorted(builders_and_tests.iteritems()):
516 print_text.append(' %s: %s' % (builder, tests))
517 parameters = {
518 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000519 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100520 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000521 'revision': options.revision,
522 }],
tandrii8c5a3532016-11-04 07:52:02 -0700523 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000524 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000525 if 'presubmit' in builder.lower():
526 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000527 if tests:
528 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700529
530 tags = [
531 'builder:%s' % builder,
532 'buildset:%s' % buildset,
533 'user_agent:git_cl_try',
534 ]
535 if master:
536 parameters['properties']['master'] = master
537 tags.append('master:%s' % master)
538
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000539 batch_req_body['builds'].append(
540 {
541 'bucket': bucket,
542 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000543 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700544 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000545 }
546 )
547
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000548 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700549 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000550 http,
551 buildbucket_put_url,
552 'PUT',
553 body=json.dumps(batch_req_body),
554 headers={'Content-Type': 'application/json'}
555 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000556 print_text.append('To see results here, run: git cl try-results')
557 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700558 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000559
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000560
tandrii221ab252016-10-06 08:12:04 -0700561def fetch_try_jobs(auth_config, changelist, buildbucket_host,
562 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700563 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000564
qyearsley53f48a12016-09-01 10:45:13 -0700565 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000566 """
tandrii221ab252016-10-06 08:12:04 -0700567 assert buildbucket_host
568 assert changelist.GetIssue(), 'CL must be uploaded first'
569 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
570 patchset = patchset or changelist.GetMostRecentPatchset()
571 assert patchset, 'CL must be uploaded first'
572
573 codereview_url = changelist.GetCodereviewServer()
574 codereview_host = urlparse.urlparse(codereview_url).hostname
575 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000576 if authenticator.has_cached_credentials():
577 http = authenticator.authorize(httplib2.Http())
578 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700579 print('Warning: Some results might be missing because %s' %
580 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700581 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000582 http = httplib2.Http()
583
584 http.force_exception_to_status_code = True
585
tandrii221ab252016-10-06 08:12:04 -0700586 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
587 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
588 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000589 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700590 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000591 params = {'tag': 'buildset:%s' % buildset}
592
593 builds = {}
594 while True:
595 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700596 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000597 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700598 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000599 for build in content.get('builds', []):
600 builds[build['id']] = build
601 if 'next_cursor' in content:
602 params['start_cursor'] = content['next_cursor']
603 else:
604 break
605 return builds
606
607
qyearsleyeab3c042016-08-24 09:18:28 -0700608def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000609 """Prints nicely result of fetch_try_jobs."""
610 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700611 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000612 return
613
614 # Make a copy, because we'll be modifying builds dictionary.
615 builds = builds.copy()
616 builder_names_cache = {}
617
618 def get_builder(b):
619 try:
620 return builder_names_cache[b['id']]
621 except KeyError:
622 try:
623 parameters = json.loads(b['parameters_json'])
624 name = parameters['builder_name']
625 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700626 print('WARNING: failed to get builder name for build %s: %s' % (
627 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000628 name = None
629 builder_names_cache[b['id']] = name
630 return name
631
632 def get_bucket(b):
633 bucket = b['bucket']
634 if bucket.startswith('master.'):
635 return bucket[len('master.'):]
636 return bucket
637
638 if options.print_master:
639 name_fmt = '%%-%ds %%-%ds' % (
640 max(len(str(get_bucket(b))) for b in builds.itervalues()),
641 max(len(str(get_builder(b))) for b in builds.itervalues()))
642 def get_name(b):
643 return name_fmt % (get_bucket(b), get_builder(b))
644 else:
645 name_fmt = '%%-%ds' % (
646 max(len(str(get_builder(b))) for b in builds.itervalues()))
647 def get_name(b):
648 return name_fmt % get_builder(b)
649
650 def sort_key(b):
651 return b['status'], b.get('result'), get_name(b), b.get('url')
652
653 def pop(title, f, color=None, **kwargs):
654 """Pop matching builds from `builds` dict and print them."""
655
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000656 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000657 colorize = str
658 else:
659 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
660
661 result = []
662 for b in builds.values():
663 if all(b.get(k) == v for k, v in kwargs.iteritems()):
664 builds.pop(b['id'])
665 result.append(b)
666 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700667 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000668 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700669 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000670
671 total = len(builds)
672 pop(status='COMPLETED', result='SUCCESS',
673 title='Successes:', color=Fore.GREEN,
674 f=lambda b: (get_name(b), b.get('url')))
675 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
676 title='Infra Failures:', color=Fore.MAGENTA,
677 f=lambda b: (get_name(b), b.get('url')))
678 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
679 title='Failures:', color=Fore.RED,
680 f=lambda b: (get_name(b), b.get('url')))
681 pop(status='COMPLETED', result='CANCELED',
682 title='Canceled:', color=Fore.MAGENTA,
683 f=lambda b: (get_name(b),))
684 pop(status='COMPLETED', result='FAILURE',
685 failure_reason='INVALID_BUILD_DEFINITION',
686 title='Wrong master/builder name:', color=Fore.MAGENTA,
687 f=lambda b: (get_name(b),))
688 pop(status='COMPLETED', result='FAILURE',
689 title='Other failures:',
690 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
691 pop(status='COMPLETED',
692 title='Other finished:',
693 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
694 pop(status='STARTED',
695 title='Started:', color=Fore.YELLOW,
696 f=lambda b: (get_name(b), b.get('url')))
697 pop(status='SCHEDULED',
698 title='Scheduled:',
699 f=lambda b: (get_name(b), 'id=%s' % b['id']))
700 # The last section is just in case buildbucket API changes OR there is a bug.
701 pop(title='Other:',
702 f=lambda b: (get_name(b), 'id=%s' % b['id']))
703 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700704 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000705
706
qyearsley53f48a12016-09-01 10:45:13 -0700707def write_try_results_json(output_file, builds):
708 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
709
710 The input |builds| dict is assumed to be generated by Buildbucket.
711 Buildbucket documentation: http://goo.gl/G0s101
712 """
713
714 def convert_build_dict(build):
715 return {
716 'buildbucket_id': build.get('id'),
717 'status': build.get('status'),
718 'result': build.get('result'),
719 'bucket': build.get('bucket'),
720 'builder_name': json.loads(
721 build.get('parameters_json', '{}')).get('builder_name'),
722 'failure_reason': build.get('failure_reason'),
723 'url': build.get('url'),
724 }
725
726 converted = []
727 for _, build in sorted(builds.items()):
728 converted.append(convert_build_dict(build))
729 write_json(output_file, converted)
730
731
iannucci@chromium.org79540052012-10-19 23:15:26 +0000732def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000733 """Prints statistics about the change to the user."""
734 # --no-ext-diff is broken in some versions of Git, so try to work around
735 # this by overriding the environment (but there is still a problem if the
736 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000737 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000738 if 'GIT_EXTERNAL_DIFF' in env:
739 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000740
741 if find_copies:
scottmgb84b5e32016-11-10 09:25:33 -0800742 similarity_options = ['-l100000', '-C%s' % similarity]
iannucci@chromium.org79540052012-10-19 23:15:26 +0000743 else:
744 similarity_options = ['-M%s' % similarity]
745
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000746 try:
747 stdout = sys.stdout.fileno()
748 except AttributeError:
749 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000750 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000751 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000752 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000753 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000754
755
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000756class BuildbucketResponseException(Exception):
757 pass
758
759
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000760class Settings(object):
761 def __init__(self):
762 self.default_server = None
763 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000764 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000765 self.tree_status_url = None
766 self.viewvc_url = None
767 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000768 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000769 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000770 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000771 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000772 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000773 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000774
775 def LazyUpdateIfNeeded(self):
776 """Updates the settings from a codereview.settings file, if available."""
777 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000778 # The only value that actually changes the behavior is
779 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000780 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000781 error_ok=True
782 ).strip().lower()
783
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000784 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000785 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000786 LoadCodereviewSettingsFromFile(cr_settings_file)
787 self.updated = True
788
789 def GetDefaultServerUrl(self, error_ok=False):
790 if not self.default_server:
791 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000792 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000793 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000794 if error_ok:
795 return self.default_server
796 if not self.default_server:
797 error_message = ('Could not find settings file. You must configure '
798 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000799 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000800 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000801 return self.default_server
802
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000803 @staticmethod
804 def GetRelativeRoot():
805 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000806
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000807 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000808 if self.root is None:
809 self.root = os.path.abspath(self.GetRelativeRoot())
810 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000811
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000812 def GetGitMirror(self, remote='origin'):
813 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000814 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000815 if not os.path.isdir(local_url):
816 return None
817 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
818 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100819 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 16:44:13 +0100820 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000821 if mirror.exists():
822 return mirror
823 return None
824
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000825 def GetTreeStatusUrl(self, error_ok=False):
826 if not self.tree_status_url:
827 error_message = ('You must configure your tree status URL by running '
828 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000829 self.tree_status_url = self._GetRietveldConfig(
830 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000831 return self.tree_status_url
832
833 def GetViewVCUrl(self):
834 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000835 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000836 return self.viewvc_url
837
Mark Mentovai57c47212017-03-09 11:14:09 -0500838 def GetBugLineFormat(self):
839 # rietveld.bug-line-format should have a %s where the list of bugs should
840 # go. This is a bit of a quirk, because normal people will always want the
841 # bug list to go right after a prefix like BUG= or Bug:. The %s format
842 # approach is used strictly because there isn't a great way to carry the
843 # desired space after Bug: all the way from codereview.settings to here
844 # without treating : specially or inventing a quoting scheme.
845 bug_line_format = self._GetRietveldConfig('bug-line-format', error_ok=True)
846 if not bug_line_format:
847 # TODO(tandrii): change this to 'Bug: %s' to be a proper Gerrit footer.
848 bug_line_format = 'BUG=%s'
849 return bug_line_format
850
rmistry@google.com90752582014-01-14 21:04:50 +0000851 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000852 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000853
rmistry@google.com78948ed2015-07-08 23:09:57 +0000854 def GetIsSkipDependencyUpload(self, branch_name):
855 """Returns true if specified branch should skip dep uploads."""
856 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
857 error_ok=True)
858
rmistry@google.com5626a922015-02-26 14:03:30 +0000859 def GetRunPostUploadHook(self):
860 run_post_upload_hook = self._GetRietveldConfig(
861 'run-post-upload-hook', error_ok=True)
862 return run_post_upload_hook == "True"
863
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000864 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000865 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000866
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000867 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000868 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000869
ukai@chromium.orge8077812012-02-03 03:41:46 +0000870 def GetIsGerrit(self):
871 """Return true if this repo is assosiated with gerrit code review system."""
872 if self.is_gerrit is None:
873 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
874 return self.is_gerrit
875
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000876 def GetSquashGerritUploads(self):
877 """Return true if uploads to Gerrit should be squashed by default."""
878 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700879 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
880 if self.squash_gerrit_uploads is None:
881 # Default is squash now (http://crbug.com/611892#c23).
882 self.squash_gerrit_uploads = not (
883 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
884 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000885 return self.squash_gerrit_uploads
886
tandriia60502f2016-06-20 02:01:53 -0700887 def GetSquashGerritUploadsOverride(self):
888 """Return True or False if codereview.settings should be overridden.
889
890 Returns None if no override has been defined.
891 """
892 # See also http://crbug.com/611892#c23
893 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
894 error_ok=True).strip()
895 if result == 'true':
896 return True
897 if result == 'false':
898 return False
899 return None
900
tandrii@chromium.org28253532016-04-14 13:46:56 +0000901 def GetGerritSkipEnsureAuthenticated(self):
902 """Return True if EnsureAuthenticated should not be done for Gerrit
903 uploads."""
904 if self.gerrit_skip_ensure_authenticated is None:
905 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000906 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000907 error_ok=True).strip() == 'true')
908 return self.gerrit_skip_ensure_authenticated
909
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000910 def GetGitEditor(self):
911 """Return the editor specified in the git config, or None if none is."""
912 if self.git_editor is None:
913 self.git_editor = self._GetConfig('core.editor', error_ok=True)
914 return self.git_editor or None
915
thestig@chromium.org44202a22014-03-11 19:22:18 +0000916 def GetLintRegex(self):
917 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
918 DEFAULT_LINT_REGEX)
919
920 def GetLintIgnoreRegex(self):
921 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
922 DEFAULT_LINT_IGNORE_REGEX)
923
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000924 def GetProject(self):
925 if not self.project:
926 self.project = self._GetRietveldConfig('project', error_ok=True)
927 return self.project
928
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000929 def _GetRietveldConfig(self, param, **kwargs):
930 return self._GetConfig('rietveld.' + param, **kwargs)
931
rmistry@google.com78948ed2015-07-08 23:09:57 +0000932 def _GetBranchConfig(self, branch_name, param, **kwargs):
933 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
934
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000935 def _GetConfig(self, param, **kwargs):
936 self.LazyUpdateIfNeeded()
937 return RunGit(['config', param], **kwargs).strip()
938
939
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100940@contextlib.contextmanager
941def _get_gerrit_project_config_file(remote_url):
942 """Context manager to fetch and store Gerrit's project.config from
943 refs/meta/config branch and store it in temp file.
944
945 Provides a temporary filename or None if there was error.
946 """
947 error, _ = RunGitWithCode([
948 'fetch', remote_url,
949 '+refs/meta/config:refs/git_cl/meta/config'])
950 if error:
951 # Ref doesn't exist or isn't accessible to current user.
952 print('WARNING: failed to fetch project config for %s: %s' %
953 (remote_url, error))
954 yield None
955 return
956
957 error, project_config_data = RunGitWithCode(
958 ['show', 'refs/git_cl/meta/config:project.config'])
959 if error:
960 print('WARNING: project.config file not found')
961 yield None
962 return
963
964 with gclient_utils.temporary_directory() as tempdir:
965 project_config_file = os.path.join(tempdir, 'project.config')
966 gclient_utils.FileWrite(project_config_file, project_config_data)
967 yield project_config_file
968
969
970def _is_git_numberer_enabled(remote_url, remote_ref):
971 """Returns True if Git Numberer is enabled on this ref."""
972 # TODO(tandrii): this should be deleted once repos below are 100% on Gerrit.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100973 KNOWN_PROJECTS_WHITELIST = [
974 'chromium/src',
975 'external/webrtc',
976 'v8/v8',
977 ]
978
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100979 assert remote_ref and remote_ref.startswith('refs/'), remote_ref
980 url_parts = urlparse.urlparse(remote_url)
981 project_name = url_parts.path.lstrip('/').rstrip('git./')
982 for known in KNOWN_PROJECTS_WHITELIST:
983 if project_name.endswith(known):
984 break
985 else:
986 # Early exit to avoid extra fetches for repos that aren't using Git
987 # Numberer.
988 return False
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100989
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100990 with _get_gerrit_project_config_file(remote_url) as project_config_file:
991 if project_config_file is None:
992 # Failed to fetch project.config, which shouldn't happen on open source
993 # repos KNOWN_PROJECTS_WHITELIST.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100994 return False
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100995 def get_opts(x):
996 code, out = RunGitWithCode(
997 ['config', '-f', project_config_file, '--get-all',
998 'plugin.git-numberer.validate-%s-refglob' % x])
999 if code == 0:
1000 return out.strip().splitlines()
1001 return []
1002 enabled, disabled = map(get_opts, ['enabled', 'disabled'])
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001003
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01001004 logging.info('validator config enabled %s disabled %s refglobs for '
1005 '(this ref: %s)', enabled, disabled, remote_ref)
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +00001006
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01001007 def match_refglobs(refglobs):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001008 for refglob in refglobs:
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01001009 if remote_ref == refglob or fnmatch.fnmatch(remote_ref, refglob):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001010 return True
1011 return False
1012
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01001013 if match_refglobs(disabled):
1014 return False
1015 return match_refglobs(enabled)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001016
1017
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001018def ShortBranchName(branch):
1019 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001020 return branch.replace('refs/heads/', '', 1)
1021
1022
1023def GetCurrentBranchRef():
1024 """Returns branch ref (e.g., refs/heads/master) or None."""
1025 return RunGit(['symbolic-ref', 'HEAD'],
1026 stderr=subprocess2.VOID, error_ok=True).strip() or None
1027
1028
1029def GetCurrentBranch():
1030 """Returns current branch or None.
1031
1032 For refs/heads/* branches, returns just last part. For others, full ref.
1033 """
1034 branchref = GetCurrentBranchRef()
1035 if branchref:
1036 return ShortBranchName(branchref)
1037 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001038
1039
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001040class _CQState(object):
1041 """Enum for states of CL with respect to Commit Queue."""
1042 NONE = 'none'
1043 DRY_RUN = 'dry_run'
1044 COMMIT = 'commit'
1045
1046 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1047
1048
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001049class _ParsedIssueNumberArgument(object):
1050 def __init__(self, issue=None, patchset=None, hostname=None):
1051 self.issue = issue
1052 self.patchset = patchset
1053 self.hostname = hostname
1054
1055 @property
1056 def valid(self):
1057 return self.issue is not None
1058
1059
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001060def ParseIssueNumberArgument(arg):
1061 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1062 fail_result = _ParsedIssueNumberArgument()
1063
1064 if arg.isdigit():
1065 return _ParsedIssueNumberArgument(issue=int(arg))
1066 if not arg.startswith('http'):
1067 return fail_result
1068 url = gclient_utils.UpgradeToHttps(arg)
1069 try:
1070 parsed_url = urlparse.urlparse(url)
1071 except ValueError:
1072 return fail_result
1073 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
1074 tmp = cls.ParseIssueURL(parsed_url)
1075 if tmp is not None:
1076 return tmp
1077 return fail_result
1078
1079
Aaron Gablea45ee112016-11-22 15:14:38 -08001080class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001081 def __init__(self, issue, url):
1082 self.issue = issue
1083 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001084 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001085
1086 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001087 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001088 self.issue, self.url)
1089
1090
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001091class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001092 """Changelist works with one changelist in local branch.
1093
1094 Supports two codereview backends: Rietveld or Gerrit, selected at object
1095 creation.
1096
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001097 Notes:
1098 * Not safe for concurrent multi-{thread,process} use.
1099 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001100 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001101 """
1102
1103 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1104 """Create a new ChangeList instance.
1105
1106 If issue is given, the codereview must be given too.
1107
1108 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1109 Otherwise, it's decided based on current configuration of the local branch,
1110 with default being 'rietveld' for backwards compatibility.
1111 See _load_codereview_impl for more details.
1112
1113 **kwargs will be passed directly to codereview implementation.
1114 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001115 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001116 global settings
1117 if not settings:
1118 # Happens when git_cl.py is used as a utility library.
1119 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001120
1121 if issue:
1122 assert codereview, 'codereview must be known, if issue is known'
1123
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001124 self.branchref = branchref
1125 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001126 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001127 self.branch = ShortBranchName(self.branchref)
1128 else:
1129 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001130 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001131 self.lookedup_issue = False
1132 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001133 self.has_description = False
1134 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001135 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001136 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001137 self.cc = None
1138 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001139 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001140
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001141 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001142 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001143 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001144 assert self._codereview_impl
1145 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001146
1147 def _load_codereview_impl(self, codereview=None, **kwargs):
1148 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001149 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1150 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1151 self._codereview = codereview
1152 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001153 return
1154
1155 # Automatic selection based on issue number set for a current branch.
1156 # Rietveld takes precedence over Gerrit.
1157 assert not self.issue
1158 # Whether we find issue or not, we are doing the lookup.
1159 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001160 if self.GetBranch():
1161 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1162 issue = _git_get_branch_config_value(
1163 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1164 if issue:
1165 self._codereview = codereview
1166 self._codereview_impl = cls(self, **kwargs)
1167 self.issue = int(issue)
1168 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001169
1170 # No issue is set for this branch, so decide based on repo-wide settings.
1171 return self._load_codereview_impl(
1172 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1173 **kwargs)
1174
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001175 def IsGerrit(self):
1176 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001177
1178 def GetCCList(self):
1179 """Return the users cc'd on this CL.
1180
agable92bec4f2016-08-24 09:27:27 -07001181 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001182 """
1183 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001184 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001185 more_cc = ','.join(self.watchers)
1186 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1187 return self.cc
1188
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001189 def GetCCListWithoutDefault(self):
1190 """Return the users cc'd on this CL excluding default ones."""
1191 if self.cc is None:
1192 self.cc = ','.join(self.watchers)
1193 return self.cc
1194
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001195 def SetWatchers(self, watchers):
1196 """Set the list of email addresses that should be cc'd based on the changed
1197 files in this CL.
1198 """
1199 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001200
1201 def GetBranch(self):
1202 """Returns the short branch name, e.g. 'master'."""
1203 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001204 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001205 if not branchref:
1206 return None
1207 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001208 self.branch = ShortBranchName(self.branchref)
1209 return self.branch
1210
1211 def GetBranchRef(self):
1212 """Returns the full branch name, e.g. 'refs/heads/master'."""
1213 self.GetBranch() # Poke the lazy loader.
1214 return self.branchref
1215
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001216 def ClearBranch(self):
1217 """Clears cached branch data of this object."""
1218 self.branch = self.branchref = None
1219
tandrii5d48c322016-08-18 16:19:37 -07001220 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1221 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1222 kwargs['branch'] = self.GetBranch()
1223 return _git_get_branch_config_value(key, default, **kwargs)
1224
1225 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1226 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1227 assert self.GetBranch(), (
1228 'this CL must have an associated branch to %sset %s%s' %
1229 ('un' if value is None else '',
1230 key,
1231 '' if value is None else ' to %r' % value))
1232 kwargs['branch'] = self.GetBranch()
1233 return _git_set_branch_config_value(key, value, **kwargs)
1234
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001235 @staticmethod
1236 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001237 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001238 e.g. 'origin', 'refs/heads/master'
1239 """
1240 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001241 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1242
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001243 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001244 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001245 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001246 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1247 error_ok=True).strip()
1248 if upstream_branch:
1249 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001250 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001251 # Else, try to guess the origin remote.
1252 remote_branches = RunGit(['branch', '-r']).split()
1253 if 'origin/master' in remote_branches:
1254 # Fall back on origin/master if it exits.
1255 remote = 'origin'
1256 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001257 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001258 DieWithError(
1259 'Unable to determine default branch to diff against.\n'
1260 'Either pass complete "git diff"-style arguments, like\n'
1261 ' git cl upload origin/master\n'
1262 'or verify this branch is set up to track another \n'
1263 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001264
1265 return remote, upstream_branch
1266
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001267 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001268 upstream_branch = self.GetUpstreamBranch()
1269 if not BranchExists(upstream_branch):
1270 DieWithError('The upstream for the current branch (%s) does not exist '
1271 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001272 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001273 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001274
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001275 def GetUpstreamBranch(self):
1276 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001277 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001278 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001279 upstream_branch = upstream_branch.replace('refs/heads/',
1280 'refs/remotes/%s/' % remote)
1281 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1282 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001283 self.upstream_branch = upstream_branch
1284 return self.upstream_branch
1285
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001286 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001287 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001288 remote, branch = None, self.GetBranch()
1289 seen_branches = set()
1290 while branch not in seen_branches:
1291 seen_branches.add(branch)
1292 remote, branch = self.FetchUpstreamTuple(branch)
1293 branch = ShortBranchName(branch)
1294 if remote != '.' or branch.startswith('refs/remotes'):
1295 break
1296 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001297 remotes = RunGit(['remote'], error_ok=True).split()
1298 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001299 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001300 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001301 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001302 logging.warn('Could not determine which remote this change is '
1303 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001304 else:
1305 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001306 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001307 branch = 'HEAD'
1308 if branch.startswith('refs/remotes'):
1309 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001310 elif branch.startswith('refs/branch-heads/'):
1311 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001312 else:
1313 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001314 return self._remote
1315
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001316 def GitSanityChecks(self, upstream_git_obj):
1317 """Checks git repo status and ensures diff is from local commits."""
1318
sbc@chromium.org79706062015-01-14 21:18:12 +00001319 if upstream_git_obj is None:
1320 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001321 print('ERROR: unable to determine current branch (detached HEAD?)',
1322 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001323 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001324 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001325 return False
1326
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001327 # Verify the commit we're diffing against is in our current branch.
1328 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1329 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1330 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001331 print('ERROR: %s is not in the current branch. You may need to rebase '
1332 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001333 return False
1334
1335 # List the commits inside the diff, and verify they are all local.
1336 commits_in_diff = RunGit(
1337 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1338 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1339 remote_branch = remote_branch.strip()
1340 if code != 0:
1341 _, remote_branch = self.GetRemoteBranch()
1342
1343 commits_in_remote = RunGit(
1344 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1345
1346 common_commits = set(commits_in_diff) & set(commits_in_remote)
1347 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001348 print('ERROR: Your diff contains %d commits already in %s.\n'
1349 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1350 'the diff. If you are using a custom git flow, you can override'
1351 ' the reference used for this check with "git config '
1352 'gitcl.remotebranch <git-ref>".' % (
1353 len(common_commits), remote_branch, upstream_git_obj),
1354 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001355 return False
1356 return True
1357
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001358 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001359 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001360
1361 Returns None if it is not set.
1362 """
tandrii5d48c322016-08-18 16:19:37 -07001363 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001364
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001365 def GetRemoteUrl(self):
1366 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1367
1368 Returns None if there is no remote.
1369 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001370 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001371 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1372
1373 # If URL is pointing to a local directory, it is probably a git cache.
1374 if os.path.isdir(url):
1375 url = RunGit(['config', 'remote.%s.url' % remote],
1376 error_ok=True,
1377 cwd=url).strip()
1378 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001379
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001380 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001381 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001382 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001383 self.issue = self._GitGetBranchConfigValue(
1384 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001385 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001386 return self.issue
1387
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001388 def GetIssueURL(self):
1389 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001390 issue = self.GetIssue()
1391 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001392 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001393 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001394
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001395 def GetDescription(self, pretty=False, force=False):
1396 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001397 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001398 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001399 self.has_description = True
1400 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001401 # Set width to 72 columns + 2 space indent.
1402 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001403 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001404 lines = self.description.splitlines()
1405 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001406 return self.description
1407
1408 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001409 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001410 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001411 self.patchset = self._GitGetBranchConfigValue(
1412 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001413 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001414 return self.patchset
1415
1416 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001417 """Set this branch's patchset. If patchset=0, clears the patchset."""
1418 assert self.GetBranch()
1419 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001420 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001421 else:
1422 self.patchset = int(patchset)
1423 self._GitSetBranchConfigValue(
1424 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001425
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001426 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001427 """Set this branch's issue. If issue isn't given, clears the issue."""
1428 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001429 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001430 issue = int(issue)
1431 self._GitSetBranchConfigValue(
1432 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001433 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001434 codereview_server = self._codereview_impl.GetCodereviewServer()
1435 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001436 self._GitSetBranchConfigValue(
1437 self._codereview_impl.CodereviewServerConfigKey(),
1438 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001439 else:
tandrii5d48c322016-08-18 16:19:37 -07001440 # Reset all of these just to be clean.
1441 reset_suffixes = [
1442 'last-upload-hash',
1443 self._codereview_impl.IssueConfigKey(),
1444 self._codereview_impl.PatchsetConfigKey(),
1445 self._codereview_impl.CodereviewServerConfigKey(),
1446 ] + self._PostUnsetIssueProperties()
1447 for prop in reset_suffixes:
1448 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001449 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001450 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001451
dnjba1b0f32016-09-02 12:37:42 -07001452 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001453 if not self.GitSanityChecks(upstream_branch):
1454 DieWithError('\nGit sanity check failure')
1455
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001456 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001457 if not root:
1458 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001459 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001460
1461 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001462 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001463 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001464 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001465 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001466 except subprocess2.CalledProcessError:
1467 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001468 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001469 'This branch probably doesn\'t exist anymore. To reset the\n'
1470 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001471 ' git branch --set-upstream-to origin/master %s\n'
1472 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001473 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001474
maruel@chromium.org52424302012-08-29 15:14:30 +00001475 issue = self.GetIssue()
1476 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001477 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001478 description = self.GetDescription()
1479 else:
1480 # If the change was never uploaded, use the log messages of all commits
1481 # up to the branch point, as git cl upload will prefill the description
1482 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001483 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1484 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001485
1486 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001487 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001488 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001489 name,
1490 description,
1491 absroot,
1492 files,
1493 issue,
1494 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001495 author,
1496 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001497
dsansomee2d6fd92016-09-08 00:10:47 -07001498 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001499 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001500 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001501 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001502
1503 def RunHook(self, committing, may_prompt, verbose, change):
1504 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1505 try:
1506 return presubmit_support.DoPresubmitChecks(change, committing,
1507 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1508 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001509 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1510 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001511 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001512 DieWithError(
1513 ('%s\nMaybe your depot_tools is out of date?\n'
1514 'If all fails, contact maruel@') % e)
1515
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001516 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1517 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001518 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1519 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001520 else:
1521 # Assume url.
1522 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1523 urlparse.urlparse(issue_arg))
1524 if not parsed_issue_arg or not parsed_issue_arg.valid:
1525 DieWithError('Failed to parse issue argument "%s". '
1526 'Must be an issue number or a valid URL.' % issue_arg)
1527 return self._codereview_impl.CMDPatchWithParsedIssue(
1528 parsed_issue_arg, reject, nocommit, directory)
1529
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001530 def CMDUpload(self, options, git_diff_args, orig_args):
1531 """Uploads a change to codereview."""
1532 if git_diff_args:
1533 # TODO(ukai): is it ok for gerrit case?
1534 base_branch = git_diff_args[0]
1535 else:
1536 if self.GetBranch() is None:
1537 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1538
1539 # Default to diffing against common ancestor of upstream branch
1540 base_branch = self.GetCommonAncestorWithUpstream()
1541 git_diff_args = [base_branch, 'HEAD']
1542
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001543 # Fast best-effort checks to abort before running potentially
1544 # expensive hooks if uploading is likely to fail anyway. Passing these
1545 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001546 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001547 self._codereview_impl.EnsureCanUploadPatchset()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001548
1549 # Apply watchlists on upload.
1550 change = self.GetChange(base_branch, None)
1551 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1552 files = [f.LocalPath() for f in change.AffectedFiles()]
1553 if not options.bypass_watchlists:
1554 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1555
1556 if not options.bypass_hooks:
1557 if options.reviewers or options.tbr_owners:
1558 # Set the reviewer list now so that presubmit checks can access it.
1559 change_description = ChangeDescription(change.FullDescriptionText())
1560 change_description.update_reviewers(options.reviewers,
1561 options.tbr_owners,
1562 change)
1563 change.SetDescriptionText(change_description.description)
1564 hook_results = self.RunHook(committing=False,
1565 may_prompt=not options.force,
1566 verbose=options.verbose,
1567 change=change)
1568 if not hook_results.should_continue():
1569 return 1
1570 if not options.reviewers and hook_results.reviewers:
1571 options.reviewers = hook_results.reviewers.split(',')
1572
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001573 # TODO(tandrii): Checking local patchset against remote patchset is only
1574 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1575 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001576 latest_patchset = self.GetMostRecentPatchset()
1577 local_patchset = self.GetPatchset()
1578 if (latest_patchset and local_patchset and
1579 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001580 print('The last upload made from this repository was patchset #%d but '
1581 'the most recent patchset on the server is #%d.'
1582 % (local_patchset, latest_patchset))
1583 print('Uploading will still work, but if you\'ve uploaded to this '
1584 'issue from another machine or branch the patch you\'re '
1585 'uploading now might not include those changes.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001586 confirm_or_exit(action='upload')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001587
1588 print_stats(options.similarity, options.find_copies, git_diff_args)
1589 ret = self.CMDUploadChange(options, git_diff_args, change)
1590 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001591 if options.use_commit_queue:
1592 self.SetCQState(_CQState.COMMIT)
1593 elif options.cq_dry_run:
1594 self.SetCQState(_CQState.DRY_RUN)
1595
tandrii5d48c322016-08-18 16:19:37 -07001596 _git_set_branch_config_value('last-upload-hash',
1597 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001598 # Run post upload hooks, if specified.
1599 if settings.GetRunPostUploadHook():
1600 presubmit_support.DoPostUploadExecuter(
1601 change,
1602 self,
1603 settings.GetRoot(),
1604 options.verbose,
1605 sys.stdout)
1606
1607 # Upload all dependencies if specified.
1608 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001609 print()
1610 print('--dependencies has been specified.')
1611 print('All dependent local branches will be re-uploaded.')
1612 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001613 # Remove the dependencies flag from args so that we do not end up in a
1614 # loop.
1615 orig_args.remove('--dependencies')
1616 ret = upload_branch_deps(self, orig_args)
1617 return ret
1618
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001619 def SetCQState(self, new_state):
1620 """Update the CQ state for latest patchset.
1621
1622 Issue must have been already uploaded and known.
1623 """
1624 assert new_state in _CQState.ALL_STATES
1625 assert self.GetIssue()
1626 return self._codereview_impl.SetCQState(new_state)
1627
qyearsley1fdfcb62016-10-24 13:22:03 -07001628 def TriggerDryRun(self):
1629 """Triggers a dry run and prints a warning on failure."""
1630 # TODO(qyearsley): Either re-use this method in CMDset_commit
1631 # and CMDupload, or change CMDtry to trigger dry runs with
1632 # just SetCQState, and catch keyboard interrupt and other
1633 # errors in that method.
1634 try:
1635 self.SetCQState(_CQState.DRY_RUN)
1636 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1637 return 0
1638 except KeyboardInterrupt:
1639 raise
1640 except:
1641 print('WARNING: failed to trigger CQ Dry Run.\n'
1642 'Either:\n'
1643 ' * your project has no CQ\n'
1644 ' * you don\'t have permission to trigger Dry Run\n'
1645 ' * bug in this code (see stack trace below).\n'
1646 'Consider specifying which bots to trigger manually '
1647 'or asking your project owners for permissions '
1648 'or contacting Chrome Infrastructure team at '
1649 'https://www.chromium.org/infra\n\n')
1650 # Still raise exception so that stack trace is printed.
1651 raise
1652
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001653 # Forward methods to codereview specific implementation.
1654
1655 def CloseIssue(self):
1656 return self._codereview_impl.CloseIssue()
1657
1658 def GetStatus(self):
1659 return self._codereview_impl.GetStatus()
1660
1661 def GetCodereviewServer(self):
1662 return self._codereview_impl.GetCodereviewServer()
1663
tandriide281ae2016-10-12 06:02:30 -07001664 def GetIssueOwner(self):
1665 """Get owner from codereview, which may differ from this checkout."""
1666 return self._codereview_impl.GetIssueOwner()
1667
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001668 def GetApprovingReviewers(self):
1669 return self._codereview_impl.GetApprovingReviewers()
1670
1671 def GetMostRecentPatchset(self):
1672 return self._codereview_impl.GetMostRecentPatchset()
1673
tandriide281ae2016-10-12 06:02:30 -07001674 def CannotTriggerTryJobReason(self):
1675 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1676 return self._codereview_impl.CannotTriggerTryJobReason()
1677
tandrii8c5a3532016-11-04 07:52:02 -07001678 def GetTryjobProperties(self, patchset=None):
1679 """Returns dictionary of properties to launch tryjob."""
1680 return self._codereview_impl.GetTryjobProperties(patchset=patchset)
1681
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001682 def __getattr__(self, attr):
1683 # This is because lots of untested code accesses Rietveld-specific stuff
1684 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001685 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001686 # Note that child method defines __getattr__ as well, and forwards it here,
1687 # because _RietveldChangelistImpl is not cleaned up yet, and given
1688 # deprecation of Rietveld, it should probably be just removed.
1689 # Until that time, avoid infinite recursion by bypassing __getattr__
1690 # of implementation class.
1691 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001692
1693
1694class _ChangelistCodereviewBase(object):
1695 """Abstract base class encapsulating codereview specifics of a changelist."""
1696 def __init__(self, changelist):
1697 self._changelist = changelist # instance of Changelist
1698
1699 def __getattr__(self, attr):
1700 # Forward methods to changelist.
1701 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1702 # _RietveldChangelistImpl to avoid this hack?
1703 return getattr(self._changelist, attr)
1704
1705 def GetStatus(self):
1706 """Apply a rough heuristic to give a simple summary of an issue's review
1707 or CQ status, assuming adherence to a common workflow.
1708
1709 Returns None if no issue for this branch, or specific string keywords.
1710 """
1711 raise NotImplementedError()
1712
1713 def GetCodereviewServer(self):
1714 """Returns server URL without end slash, like "https://codereview.com"."""
1715 raise NotImplementedError()
1716
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001717 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001718 """Fetches and returns description from the codereview server."""
1719 raise NotImplementedError()
1720
tandrii5d48c322016-08-18 16:19:37 -07001721 @classmethod
1722 def IssueConfigKey(cls):
1723 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001724 raise NotImplementedError()
1725
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001726 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001727 def PatchsetConfigKey(cls):
1728 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001729 raise NotImplementedError()
1730
tandrii5d48c322016-08-18 16:19:37 -07001731 @classmethod
1732 def CodereviewServerConfigKey(cls):
1733 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001734 raise NotImplementedError()
1735
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001736 def _PostUnsetIssueProperties(self):
1737 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001738 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001739
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001740 def GetRieveldObjForPresubmit(self):
1741 # This is an unfortunate Rietveld-embeddedness in presubmit.
1742 # For non-Rietveld codereviews, this probably should return a dummy object.
1743 raise NotImplementedError()
1744
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001745 def GetGerritObjForPresubmit(self):
1746 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1747 return None
1748
dsansomee2d6fd92016-09-08 00:10:47 -07001749 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001750 """Update the description on codereview site."""
1751 raise NotImplementedError()
1752
1753 def CloseIssue(self):
1754 """Closes the issue."""
1755 raise NotImplementedError()
1756
1757 def GetApprovingReviewers(self):
1758 """Returns a list of reviewers approving the change.
1759
1760 Note: not necessarily committers.
1761 """
1762 raise NotImplementedError()
1763
1764 def GetMostRecentPatchset(self):
1765 """Returns the most recent patchset number from the codereview site."""
1766 raise NotImplementedError()
1767
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001768 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1769 directory):
1770 """Fetches and applies the issue.
1771
1772 Arguments:
1773 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1774 reject: if True, reject the failed patch instead of switching to 3-way
1775 merge. Rietveld only.
1776 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1777 only.
1778 directory: switch to directory before applying the patch. Rietveld only.
1779 """
1780 raise NotImplementedError()
1781
1782 @staticmethod
1783 def ParseIssueURL(parsed_url):
1784 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1785 failed."""
1786 raise NotImplementedError()
1787
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001788 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001789 """Best effort check that user is authenticated with codereview server.
1790
1791 Arguments:
1792 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001793 refresh: whether to attempt to refresh credentials. Ignored if not
1794 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001795 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001796 raise NotImplementedError()
1797
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001798 def EnsureCanUploadPatchset(self):
1799 """Best effort check that uploading isn't supposed to fail for predictable
1800 reasons.
1801
1802 This method should raise informative exception if uploading shouldn't
1803 proceed.
1804 """
1805 pass
1806
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001807 def CMDUploadChange(self, options, args, change):
1808 """Uploads a change to codereview."""
1809 raise NotImplementedError()
1810
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001811 def SetCQState(self, new_state):
1812 """Update the CQ state for latest patchset.
1813
1814 Issue must have been already uploaded and known.
1815 """
1816 raise NotImplementedError()
1817
tandriie113dfd2016-10-11 10:20:12 -07001818 def CannotTriggerTryJobReason(self):
1819 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1820 raise NotImplementedError()
1821
tandriide281ae2016-10-12 06:02:30 -07001822 def GetIssueOwner(self):
1823 raise NotImplementedError()
1824
tandrii8c5a3532016-11-04 07:52:02 -07001825 def GetTryjobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001826 raise NotImplementedError()
1827
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001828
1829class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001830 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001831 super(_RietveldChangelistImpl, self).__init__(changelist)
1832 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001833 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001834 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001835
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001836 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001837 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001838 self._props = None
1839 self._rpc_server = None
1840
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001841 def GetCodereviewServer(self):
1842 if not self._rietveld_server:
1843 # If we're on a branch then get the server potentially associated
1844 # with that branch.
1845 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001846 self._rietveld_server = gclient_utils.UpgradeToHttps(
1847 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001848 if not self._rietveld_server:
1849 self._rietveld_server = settings.GetDefaultServerUrl()
1850 return self._rietveld_server
1851
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001852 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001853 """Best effort check that user is authenticated with Rietveld server."""
1854 if self._auth_config.use_oauth2:
1855 authenticator = auth.get_authenticator_for_host(
1856 self.GetCodereviewServer(), self._auth_config)
1857 if not authenticator.has_cached_credentials():
1858 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001859 if refresh:
1860 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001861
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001862 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001863 issue = self.GetIssue()
1864 assert issue
1865 try:
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001866 return self.RpcServer().get_description(issue, force=force).strip()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001867 except urllib2.HTTPError as e:
1868 if e.code == 404:
1869 DieWithError(
1870 ('\nWhile fetching the description for issue %d, received a '
1871 '404 (not found)\n'
1872 'error. It is likely that you deleted this '
1873 'issue on the server. If this is the\n'
1874 'case, please run\n\n'
1875 ' git cl issue 0\n\n'
1876 'to clear the association with the deleted issue. Then run '
1877 'this command again.') % issue)
1878 else:
1879 DieWithError(
1880 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1881 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001882 print('Warning: Failed to retrieve CL description due to network '
1883 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001884 return ''
1885
1886 def GetMostRecentPatchset(self):
1887 return self.GetIssueProperties()['patchsets'][-1]
1888
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001889 def GetIssueProperties(self):
1890 if self._props is None:
1891 issue = self.GetIssue()
1892 if not issue:
1893 self._props = {}
1894 else:
1895 self._props = self.RpcServer().get_issue_properties(issue, True)
1896 return self._props
1897
tandriie113dfd2016-10-11 10:20:12 -07001898 def CannotTriggerTryJobReason(self):
1899 props = self.GetIssueProperties()
1900 if not props:
1901 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1902 if props.get('closed'):
1903 return 'CL %s is closed' % self.GetIssue()
1904 if props.get('private'):
1905 return 'CL %s is private' % self.GetIssue()
1906 return None
1907
tandrii8c5a3532016-11-04 07:52:02 -07001908 def GetTryjobProperties(self, patchset=None):
1909 """Returns dictionary of properties to launch tryjob."""
1910 project = (self.GetIssueProperties() or {}).get('project')
1911 return {
1912 'issue': self.GetIssue(),
1913 'patch_project': project,
1914 'patch_storage': 'rietveld',
1915 'patchset': patchset or self.GetPatchset(),
1916 'rietveld': self.GetCodereviewServer(),
1917 }
1918
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001919 def GetApprovingReviewers(self):
1920 return get_approving_reviewers(self.GetIssueProperties())
1921
tandriide281ae2016-10-12 06:02:30 -07001922 def GetIssueOwner(self):
1923 return (self.GetIssueProperties() or {}).get('owner_email')
1924
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001925 def AddComment(self, message):
1926 return self.RpcServer().add_comment(self.GetIssue(), message)
1927
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001928 def GetStatus(self):
1929 """Apply a rough heuristic to give a simple summary of an issue's review
1930 or CQ status, assuming adherence to a common workflow.
1931
1932 Returns None if no issue for this branch, or one of the following keywords:
1933 * 'error' - error from review tool (including deleted issues)
1934 * 'unsent' - not sent for review
1935 * 'waiting' - waiting for review
1936 * 'reply' - waiting for owner to reply to review
1937 * 'lgtm' - LGTM from at least one approved reviewer
1938 * 'commit' - in the commit queue
1939 * 'closed' - closed
1940 """
1941 if not self.GetIssue():
1942 return None
1943
1944 try:
1945 props = self.GetIssueProperties()
1946 except urllib2.HTTPError:
1947 return 'error'
1948
1949 if props.get('closed'):
1950 # Issue is closed.
1951 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001952 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001953 # Issue is in the commit queue.
1954 return 'commit'
1955
1956 try:
1957 reviewers = self.GetApprovingReviewers()
1958 except urllib2.HTTPError:
1959 return 'error'
1960
1961 if reviewers:
1962 # Was LGTM'ed.
1963 return 'lgtm'
1964
1965 messages = props.get('messages') or []
1966
tandrii9d2c7a32016-06-22 03:42:45 -07001967 # Skip CQ messages that don't require owner's action.
1968 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1969 if 'Dry run:' in messages[-1]['text']:
1970 messages.pop()
1971 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1972 # This message always follows prior messages from CQ,
1973 # so skip this too.
1974 messages.pop()
1975 else:
1976 # This is probably a CQ messages warranting user attention.
1977 break
1978
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001979 if not messages:
1980 # No message was sent.
1981 return 'unsent'
1982 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001983 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001984 return 'reply'
1985 return 'waiting'
1986
dsansomee2d6fd92016-09-08 00:10:47 -07001987 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001988 self.RpcServer().update_description(self.GetIssue(), description)
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001989
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001990 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001991 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001992
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001993 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07001994 return self.SetFlags({flag: value})
1995
1996 def SetFlags(self, flags):
1997 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07001998 """
phajdan.jr68598232016-08-10 03:28:28 -07001999 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002000 try:
tandrii4b233bd2016-07-06 03:50:29 -07002001 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002002 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002003 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002004 if e.code == 404:
2005 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2006 if e.code == 403:
2007 DieWithError(
2008 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002009 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002010 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002011
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002012 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002013 """Returns an upload.RpcServer() to access this review's rietveld instance.
2014 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002015 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002016 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002017 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01002018 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002019 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002020
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002021 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002022 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002023 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002024
tandrii5d48c322016-08-18 16:19:37 -07002025 @classmethod
2026 def PatchsetConfigKey(cls):
2027 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002028
tandrii5d48c322016-08-18 16:19:37 -07002029 @classmethod
2030 def CodereviewServerConfigKey(cls):
2031 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002032
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002033 def GetRieveldObjForPresubmit(self):
2034 return self.RpcServer()
2035
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002036 def SetCQState(self, new_state):
2037 props = self.GetIssueProperties()
2038 if props.get('private'):
2039 DieWithError('Cannot set-commit on private issue')
2040
2041 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002042 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002043 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002044 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002045 else:
tandrii4b233bd2016-07-06 03:50:29 -07002046 assert new_state == _CQState.DRY_RUN
2047 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002048
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002049 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2050 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002051 # PatchIssue should never be called with a dirty tree. It is up to the
2052 # caller to check this, but just in case we assert here since the
2053 # consequences of the caller not checking this could be dire.
2054 assert(not git_common.is_dirty_git_tree('apply'))
2055 assert(parsed_issue_arg.valid)
2056 self._changelist.issue = parsed_issue_arg.issue
2057 if parsed_issue_arg.hostname:
2058 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2059
skobes6468b902016-10-24 08:45:10 -07002060 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2061 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2062 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002063 try:
skobes6468b902016-10-24 08:45:10 -07002064 scm_obj.apply_patch(patchset_object)
2065 except Exception as e:
2066 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002067 return 1
2068
2069 # If we had an issue, commit the current state and register the issue.
2070 if not nocommit:
2071 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2072 'patch from issue %(i)s at patchset '
2073 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2074 % {'i': self.GetIssue(), 'p': patchset})])
2075 self.SetIssue(self.GetIssue())
2076 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07002077 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002078 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002079 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002080 return 0
2081
2082 @staticmethod
2083 def ParseIssueURL(parsed_url):
2084 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2085 return None
wychen3c1c1722016-08-04 11:46:36 -07002086 # Rietveld patch: https://domain/<number>/#ps<patchset>
2087 match = re.match(r'/(\d+)/$', parsed_url.path)
2088 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2089 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002090 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002091 issue=int(match.group(1)),
2092 patchset=int(match2.group(1)),
2093 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002094 # Typical url: https://domain/<issue_number>[/[other]]
2095 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2096 if match:
skobes6468b902016-10-24 08:45:10 -07002097 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002098 issue=int(match.group(1)),
2099 hostname=parsed_url.netloc)
2100 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2101 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2102 if match:
skobes6468b902016-10-24 08:45:10 -07002103 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002104 issue=int(match.group(1)),
2105 patchset=int(match.group(2)),
skobes6468b902016-10-24 08:45:10 -07002106 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002107 return None
2108
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002109 def CMDUploadChange(self, options, args, change):
2110 """Upload the patch to Rietveld."""
2111 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2112 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002113 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2114 if options.emulate_svn_auto_props:
2115 upload_args.append('--emulate_svn_auto_props')
2116
2117 change_desc = None
2118
2119 if options.email is not None:
2120 upload_args.extend(['--email', options.email])
2121
2122 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002123 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002124 upload_args.extend(['--title', options.title])
2125 if options.message:
2126 upload_args.extend(['--message', options.message])
2127 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002128 print('This branch is associated with issue %s. '
2129 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002130 else:
nodirca166002016-06-27 10:59:51 -07002131 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002132 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002133 if options.message:
2134 message = options.message
2135 else:
2136 message = CreateDescriptionFromLog(args)
2137 if options.title:
2138 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002139 change_desc = ChangeDescription(message)
2140 if options.reviewers or options.tbr_owners:
2141 change_desc.update_reviewers(options.reviewers,
2142 options.tbr_owners,
2143 change)
2144 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002145 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002146
2147 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002148 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002149 return 1
2150
2151 upload_args.extend(['--message', change_desc.description])
2152 if change_desc.get_reviewers():
2153 upload_args.append('--reviewers=%s' % ','.join(
2154 change_desc.get_reviewers()))
2155 if options.send_mail:
2156 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002157 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002158 upload_args.append('--send_mail')
2159
2160 # We check this before applying rietveld.private assuming that in
2161 # rietveld.cc only addresses which we can send private CLs to are listed
2162 # if rietveld.private is set, and so we should ignore rietveld.cc only
2163 # when --private is specified explicitly on the command line.
2164 if options.private:
2165 logging.warn('rietveld.cc is ignored since private flag is specified. '
2166 'You need to review and add them manually if necessary.')
2167 cc = self.GetCCListWithoutDefault()
2168 else:
2169 cc = self.GetCCList()
2170 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002171 if change_desc.get_cced():
2172 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002173 if cc:
2174 upload_args.extend(['--cc', cc])
2175
2176 if options.private or settings.GetDefaultPrivateFlag() == "True":
2177 upload_args.append('--private')
2178
2179 upload_args.extend(['--git_similarity', str(options.similarity)])
2180 if not options.find_copies:
2181 upload_args.extend(['--git_no_find_copies'])
2182
2183 # Include the upstream repo's URL in the change -- this is useful for
2184 # projects that have their source spread across multiple repos.
2185 remote_url = self.GetGitBaseUrlFromConfig()
2186 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002187 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2188 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2189 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002190 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002191 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002192 target_ref = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002193 if target_ref:
2194 upload_args.extend(['--target_ref', target_ref])
2195
2196 # Look for dependent patchsets. See crbug.com/480453 for more details.
2197 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2198 upstream_branch = ShortBranchName(upstream_branch)
2199 if remote is '.':
2200 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002201 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002202 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002203 print()
2204 print('Skipping dependency patchset upload because git config '
2205 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2206 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002207 else:
2208 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002209 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002210 auth_config=auth_config)
2211 branch_cl_issue_url = branch_cl.GetIssueURL()
2212 branch_cl_issue = branch_cl.GetIssue()
2213 branch_cl_patchset = branch_cl.GetPatchset()
2214 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2215 upload_args.extend(
2216 ['--depends_on_patchset', '%s:%s' % (
2217 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002218 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002219 '\n'
2220 'The current branch (%s) is tracking a local branch (%s) with '
2221 'an associated CL.\n'
2222 'Adding %s/#ps%s as a dependency patchset.\n'
2223 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2224 branch_cl_patchset))
2225
2226 project = settings.GetProject()
2227 if project:
2228 upload_args.extend(['--project', project])
2229
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002230 try:
2231 upload_args = ['upload'] + upload_args + args
2232 logging.info('upload.RealMain(%s)', upload_args)
2233 issue, patchset = upload.RealMain(upload_args)
2234 issue = int(issue)
2235 patchset = int(patchset)
2236 except KeyboardInterrupt:
2237 sys.exit(1)
2238 except:
2239 # If we got an exception after the user typed a description for their
2240 # change, back up the description before re-raising.
2241 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002242 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002243 raise
2244
2245 if not self.GetIssue():
2246 self.SetIssue(issue)
2247 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002248 return 0
2249
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002250
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002251class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002252 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002253 # auth_config is Rietveld thing, kept here to preserve interface only.
2254 super(_GerritChangelistImpl, self).__init__(changelist)
2255 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002256 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002257 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002258 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002259 # Map from change number (issue) to its detail cache.
2260 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002261
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002262 if codereview_host is not None:
2263 assert not codereview_host.startswith('https://'), codereview_host
2264 self._gerrit_host = codereview_host
2265 self._gerrit_server = 'https://%s' % codereview_host
2266
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002267 def _GetGerritHost(self):
2268 # Lazy load of configs.
2269 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002270 if self._gerrit_host and '.' not in self._gerrit_host:
2271 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2272 # This happens for internal stuff http://crbug.com/614312.
2273 parsed = urlparse.urlparse(self.GetRemoteUrl())
2274 if parsed.scheme == 'sso':
2275 print('WARNING: using non https URLs for remote is likely broken\n'
2276 ' Your current remote is: %s' % self.GetRemoteUrl())
2277 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2278 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002279 return self._gerrit_host
2280
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002281 def _GetGitHost(self):
2282 """Returns git host to be used when uploading change to Gerrit."""
2283 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2284
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002285 def GetCodereviewServer(self):
2286 if not self._gerrit_server:
2287 # If we're on a branch then get the server potentially associated
2288 # with that branch.
2289 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002290 self._gerrit_server = self._GitGetBranchConfigValue(
2291 self.CodereviewServerConfigKey())
2292 if self._gerrit_server:
2293 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002294 if not self._gerrit_server:
2295 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2296 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002297 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002298 parts[0] = parts[0] + '-review'
2299 self._gerrit_host = '.'.join(parts)
2300 self._gerrit_server = 'https://%s' % self._gerrit_host
2301 return self._gerrit_server
2302
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002303 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002304 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002305 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002306
tandrii5d48c322016-08-18 16:19:37 -07002307 @classmethod
2308 def PatchsetConfigKey(cls):
2309 return 'gerritpatchset'
2310
2311 @classmethod
2312 def CodereviewServerConfigKey(cls):
2313 return 'gerritserver'
2314
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002315 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002316 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002317 if settings.GetGerritSkipEnsureAuthenticated():
2318 # For projects with unusual authentication schemes.
2319 # See http://crbug.com/603378.
2320 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002321 # Lazy-loader to identify Gerrit and Git hosts.
2322 if gerrit_util.GceAuthenticator.is_gce():
2323 return
2324 self.GetCodereviewServer()
2325 git_host = self._GetGitHost()
2326 assert self._gerrit_server and self._gerrit_host
2327 cookie_auth = gerrit_util.CookiesAuthenticator()
2328
2329 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2330 git_auth = cookie_auth.get_auth_header(git_host)
2331 if gerrit_auth and git_auth:
2332 if gerrit_auth == git_auth:
2333 return
2334 print((
2335 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2336 ' Check your %s or %s file for credentials of hosts:\n'
2337 ' %s\n'
2338 ' %s\n'
2339 ' %s') %
2340 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2341 git_host, self._gerrit_host,
2342 cookie_auth.get_new_password_message(git_host)))
2343 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002344 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002345 return
2346 else:
2347 missing = (
2348 [] if gerrit_auth else [self._gerrit_host] +
2349 [] if git_auth else [git_host])
2350 DieWithError('Credentials for the following hosts are required:\n'
2351 ' %s\n'
2352 'These are read from %s (or legacy %s)\n'
2353 '%s' % (
2354 '\n '.join(missing),
2355 cookie_auth.get_gitcookies_path(),
2356 cookie_auth.get_netrc_path(),
2357 cookie_auth.get_new_password_message(git_host)))
2358
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002359 def EnsureCanUploadPatchset(self):
2360 """Best effort check that uploading isn't supposed to fail for predictable
2361 reasons.
2362
2363 This method should raise informative exception if uploading shouldn't
2364 proceed.
2365 """
2366 if not self.GetIssue():
2367 return
2368
2369 # Warm change details cache now to avoid RPCs later, reducing latency for
2370 # developers.
2371 self.FetchDescription()
2372
2373 status = self._GetChangeDetail()['status']
2374 if status in ('MERGED', 'ABANDONED'):
2375 DieWithError('Change %s has been %s, new uploads are not allowed' %
2376 (self.GetIssueURL(),
2377 'submitted' if status == 'MERGED' else 'abandoned'))
2378
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002379 def _PostUnsetIssueProperties(self):
2380 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002381 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002382
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002383 def GetRieveldObjForPresubmit(self):
2384 class ThisIsNotRietveldIssue(object):
2385 def __nonzero__(self):
2386 # This is a hack to make presubmit_support think that rietveld is not
2387 # defined, yet still ensure that calls directly result in a decent
2388 # exception message below.
2389 return False
2390
2391 def __getattr__(self, attr):
2392 print(
2393 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2394 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2395 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2396 'or use Rietveld for codereview.\n'
2397 'See also http://crbug.com/579160.' % attr)
2398 raise NotImplementedError()
2399 return ThisIsNotRietveldIssue()
2400
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002401 def GetGerritObjForPresubmit(self):
2402 return presubmit_support.GerritAccessor(self._GetGerritHost())
2403
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002404 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002405 """Apply a rough heuristic to give a simple summary of an issue's review
2406 or CQ status, assuming adherence to a common workflow.
2407
2408 Returns None if no issue for this branch, or one of the following keywords:
2409 * 'error' - error from review tool (including deleted issues)
2410 * 'unsent' - no reviewers added
2411 * 'waiting' - waiting for review
2412 * 'reply' - waiting for owner to reply to review
Quinten Yearsley442fb642016-12-15 15:38:27 -08002413 * 'not lgtm' - Code-Review disapproval from at least one valid reviewer
tandriic2405f52016-10-10 08:13:15 -07002414 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002415 * 'commit' - in the commit queue
2416 * 'closed' - abandoned
2417 """
2418 if not self.GetIssue():
2419 return None
2420
2421 try:
2422 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002423 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002424 return 'error'
2425
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002426 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002427 return 'closed'
2428
2429 cq_label = data['labels'].get('Commit-Queue', {})
2430 if cq_label:
rmistryc9ebbd22016-10-14 12:35:54 -07002431 votes = cq_label.get('all', [])
2432 highest_vote = 0
2433 for v in votes:
2434 highest_vote = max(highest_vote, v.get('value', 0))
2435 vote_value = str(highest_vote)
2436 if vote_value != '0':
2437 # Add a '+' if the value is not 0 to match the values in the label.
2438 # The cq_label does not have negatives.
2439 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002440 vote_text = cq_label.get('values', {}).get(vote_value, '')
2441 if vote_text.lower() == 'commit':
2442 return 'commit'
2443
2444 lgtm_label = data['labels'].get('Code-Review', {})
2445 if lgtm_label:
2446 if 'rejected' in lgtm_label:
2447 return 'not lgtm'
2448 if 'approved' in lgtm_label:
2449 return 'lgtm'
2450
2451 if not data.get('reviewers', {}).get('REVIEWER', []):
2452 return 'unsent'
2453
2454 messages = data.get('messages', [])
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002455 owner = data['owner'].get('_account_id')
2456 while messages:
2457 last_message_author = messages.pop().get('author', {})
2458 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2459 # Ignore replies from CQ.
2460 continue
Andrii Shyshkalov7afd1642017-01-29 16:07:15 +01002461 if last_message_author.get('_account_id') != owner:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002462 # Some reply from non-owner.
2463 return 'reply'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002464 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002465
2466 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002467 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002468 return data['revisions'][data['current_revision']]['_number']
2469
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002470 def FetchDescription(self, force=False):
2471 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2472 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002473 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002474 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002475
dsansomee2d6fd92016-09-08 00:10:47 -07002476 def UpdateDescriptionRemote(self, description, force=False):
2477 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2478 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002479 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002480 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002481 'unpublished edit. Either publish the edit in the Gerrit web UI '
2482 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002483
2484 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2485 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002486 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002487 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002488
2489 def CloseIssue(self):
2490 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2491
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002492 def GetApprovingReviewers(self):
2493 """Returns a list of reviewers approving the change.
2494
2495 Note: not necessarily committers.
2496 """
2497 raise NotImplementedError()
2498
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002499 def SubmitIssue(self, wait_for_merge=True):
2500 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2501 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002502
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002503 def _GetChangeDetail(self, options=None, issue=None,
2504 no_cache=False):
2505 """Returns details of the issue by querying Gerrit and caching results.
2506
2507 If fresh data is needed, set no_cache=True which will clear cache and
2508 thus new data will be fetched from Gerrit.
2509 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002510 options = options or []
2511 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002512 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002513
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002514 # Optimization to avoid multiple RPCs:
2515 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2516 'CURRENT_COMMIT' not in options):
2517 options.append('CURRENT_COMMIT')
2518
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002519 # Normalize issue and options for consistent keys in cache.
2520 issue = str(issue)
2521 options = [o.upper() for o in options]
2522
2523 # Check in cache first unless no_cache is True.
2524 if no_cache:
2525 self._detail_cache.pop(issue, None)
2526 else:
2527 options_set = frozenset(options)
2528 for cached_options_set, data in self._detail_cache.get(issue, []):
2529 # Assumption: data fetched before with extra options is suitable
2530 # for return for a smaller set of options.
2531 # For example, if we cached data for
2532 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2533 # and request is for options=[CURRENT_REVISION],
2534 # THEN we can return prior cached data.
2535 if options_set.issubset(cached_options_set):
2536 return data
2537
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002538 try:
2539 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2540 options, ignore_404=False)
2541 except gerrit_util.GerritError as e:
2542 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002543 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002544 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002545
2546 self._detail_cache.setdefault(issue, []).append((frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002547 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002548
agable32978d92016-11-01 12:55:02 -07002549 def _GetChangeCommit(self, issue=None):
2550 issue = issue or self.GetIssue()
2551 assert issue, 'issue is required to query Gerrit'
2552 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2553 if not data:
Aaron Gablea45ee112016-11-22 15:14:38 -08002554 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
agable32978d92016-11-01 12:55:02 -07002555 return data
2556
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002557 def CMDLand(self, force, bypass_hooks, verbose):
2558 if git_common.is_dirty_git_tree('land'):
2559 return 1
tandriid60367b2016-06-22 05:25:12 -07002560 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2561 if u'Commit-Queue' in detail.get('labels', {}):
2562 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002563 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2564 'which can test and land changes for you. '
2565 'Are you sure you wish to bypass it?\n',
2566 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002567
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002568 differs = True
tandriic4344b52016-08-29 06:04:54 -07002569 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002570 # Note: git diff outputs nothing if there is no diff.
2571 if not last_upload or RunGit(['diff', last_upload]).strip():
2572 print('WARNING: some changes from local branch haven\'t been uploaded')
2573 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002574 if detail['current_revision'] == last_upload:
2575 differs = False
2576 else:
2577 print('WARNING: local branch contents differ from latest uploaded '
2578 'patchset')
2579 if differs:
2580 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002581 confirm_or_exit(
2582 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2583 action='submit')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002584 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2585 elif not bypass_hooks:
2586 hook_results = self.RunHook(
2587 committing=True,
2588 may_prompt=not force,
2589 verbose=verbose,
2590 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2591 if not hook_results.should_continue():
2592 return 1
2593
2594 self.SubmitIssue(wait_for_merge=True)
2595 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002596 links = self._GetChangeCommit().get('web_links', [])
2597 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002598 if link.get('name') == 'gitiles' and link.get('url'):
agable32978d92016-11-01 12:55:02 -07002599 print('Landed as %s' % link.get('url'))
2600 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002601 return 0
2602
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002603 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2604 directory):
2605 assert not reject
2606 assert not nocommit
2607 assert not directory
2608 assert parsed_issue_arg.valid
2609
2610 self._changelist.issue = parsed_issue_arg.issue
2611
2612 if parsed_issue_arg.hostname:
2613 self._gerrit_host = parsed_issue_arg.hostname
2614 self._gerrit_server = 'https://%s' % self._gerrit_host
2615
tandriic2405f52016-10-10 08:13:15 -07002616 try:
2617 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002618 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002619 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002620
2621 if not parsed_issue_arg.patchset:
2622 # Use current revision by default.
2623 revision_info = detail['revisions'][detail['current_revision']]
2624 patchset = int(revision_info['_number'])
2625 else:
2626 patchset = parsed_issue_arg.patchset
2627 for revision_info in detail['revisions'].itervalues():
2628 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2629 break
2630 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002631 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002632 (parsed_issue_arg.patchset, self.GetIssue()))
2633
2634 fetch_info = revision_info['fetch']['http']
2635 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2636 RunGit(['cherry-pick', 'FETCH_HEAD'])
2637 self.SetIssue(self.GetIssue())
2638 self.SetPatchset(patchset)
Aaron Gablea45ee112016-11-22 15:14:38 -08002639 print('Committed patch for change %i patchset %i locally' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002640 (self.GetIssue(), self.GetPatchset()))
2641 return 0
2642
2643 @staticmethod
2644 def ParseIssueURL(parsed_url):
2645 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2646 return None
2647 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2648 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2649 # Short urls like https://domain/<issue_number> can be used, but don't allow
2650 # specifying the patchset (you'd 404), but we allow that here.
2651 if parsed_url.path == '/':
2652 part = parsed_url.fragment
2653 else:
2654 part = parsed_url.path
2655 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2656 if match:
2657 return _ParsedIssueNumberArgument(
2658 issue=int(match.group(2)),
2659 patchset=int(match.group(4)) if match.group(4) else None,
2660 hostname=parsed_url.netloc)
2661 return None
2662
tandrii16e0b4e2016-06-07 10:34:28 -07002663 def _GerritCommitMsgHookCheck(self, offer_removal):
2664 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2665 if not os.path.exists(hook):
2666 return
2667 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2668 # custom developer made one.
2669 data = gclient_utils.FileRead(hook)
2670 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2671 return
2672 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002673 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002674 'and may interfere with it in subtle ways.\n'
2675 'We recommend you remove the commit-msg hook.')
2676 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002677 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002678 gclient_utils.rm_file_or_tree(hook)
2679 print('Gerrit commit-msg hook removed.')
2680 else:
2681 print('OK, will keep Gerrit commit-msg hook in place.')
2682
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002683 def CMDUploadChange(self, options, args, change):
2684 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002685 if options.squash and options.no_squash:
2686 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002687
2688 if not options.squash and not options.no_squash:
2689 # Load default for user, repo, squash=true, in this order.
2690 options.squash = settings.GetSquashGerritUploads()
2691 elif options.no_squash:
2692 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002693
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002694 # We assume the remote called "origin" is the one we want.
2695 # It is probably not worthwhile to support different workflows.
2696 gerrit_remote = 'origin'
2697
2698 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002699 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002700
Aaron Gableb56ad332017-01-06 15:24:31 -08002701 # This may be None; default fallback value is determined in logic below.
2702 title = options.title
Aaron Gable856585d2017-01-23 15:32:00 -08002703 automatic_title = False
Aaron Gableb56ad332017-01-06 15:24:31 -08002704
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002705 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002706 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002707 if self.GetIssue():
2708 # Try to get the message from a previous upload.
2709 message = self.GetDescription()
2710 if not message:
2711 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002712 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002713 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002714 if not title:
2715 default_title = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
2716 title = ask_for_data(
2717 'Title for patchset [%s]: ' % default_title) or default_title
Aaron Gable856585d2017-01-23 15:32:00 -08002718 if title == default_title:
2719 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002720 change_id = self._GetChangeDetail()['change_id']
2721 while True:
2722 footer_change_ids = git_footers.get_footer_change_id(message)
2723 if footer_change_ids == [change_id]:
2724 break
2725 if not footer_change_ids:
2726 message = git_footers.add_footer_change_id(message, change_id)
Aaron Gablea45ee112016-11-22 15:14:38 -08002727 print('WARNING: appended missing Change-Id to change description')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002728 continue
2729 # There is already a valid footer but with different or several ids.
2730 # Doing this automatically is non-trivial as we don't want to lose
2731 # existing other footers, yet we want to append just 1 desired
2732 # Change-Id. Thus, just create a new footer, but let user verify the
2733 # new description.
2734 message = '%s\n\nChange-Id: %s' % (message, change_id)
2735 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002736 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002737 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002738 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002739 'Please, check the proposed correction to the description, '
2740 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2741 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2742 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002743 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002744 if not options.force:
2745 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002746 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002747 message = change_desc.description
2748 if not message:
2749 DieWithError("Description is empty. Aborting...")
2750 # Continue the while loop.
2751 # Sanity check of this code - we should end up with proper message
2752 # footer.
2753 assert [change_id] == git_footers.get_footer_change_id(message)
2754 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002755 else: # if not self.GetIssue()
2756 if options.message:
2757 message = options.message
2758 else:
2759 message = CreateDescriptionFromLog(args)
2760 if options.title:
2761 message = options.title + '\n\n' + message
2762 change_desc = ChangeDescription(message)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002763 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002764 change_desc.prompt(bug=options.bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002765 # On first upload, patchset title is always this string, while
2766 # --title flag gets converted to first line of message.
2767 title = 'Initial upload'
Aaron Gable856585d2017-01-23 15:32:00 -08002768 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002769 if not change_desc.description:
2770 DieWithError("Description is empty. Aborting...")
2771 message = change_desc.description
2772 change_ids = git_footers.get_footer_change_id(message)
2773 if len(change_ids) > 1:
2774 DieWithError('too many Change-Id footers, at most 1 allowed.')
2775 if not change_ids:
2776 # Generate the Change-Id automatically.
2777 message = git_footers.add_footer_change_id(
2778 message, GenerateGerritChangeId(message))
2779 change_desc.set_description(message)
2780 change_ids = git_footers.get_footer_change_id(message)
2781 assert len(change_ids) == 1
2782 change_id = change_ids[0]
2783
2784 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2785 if remote is '.':
2786 # If our upstream branch is local, we base our squashed commit on its
2787 # squashed version.
2788 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2789 # Check the squashed hash of the parent.
2790 parent = RunGit(['config',
2791 'branch.%s.gerritsquashhash' % upstream_branch_name],
2792 error_ok=True).strip()
2793 # Verify that the upstream branch has been uploaded too, otherwise
2794 # Gerrit will create additional CLs when uploading.
2795 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2796 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002797 DieWithError(
Aaron Gable48746652017-01-06 11:49:21 -08002798 '\nUpload upstream branch %s first.\n'
2799 'It is likely that this branch has been rebased since its last '
2800 'upload, so you just need to upload it again.\n'
2801 '(If you uploaded it with --no-squash, then branch dependencies '
2802 'are not supported, and you should reupload with --squash.)'
Christopher Lamf732cd52017-01-24 12:40:11 +11002803 % upstream_branch_name, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002804 else:
2805 parent = self.GetCommonAncestorWithUpstream()
2806
2807 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2808 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2809 '-m', message]).strip()
2810 else:
2811 change_desc = ChangeDescription(
2812 options.message or CreateDescriptionFromLog(args))
2813 if not change_desc.description:
2814 DieWithError("Description is empty. Aborting...")
2815
2816 if not git_footers.get_footer_change_id(change_desc.description):
2817 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002818 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2819 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002820 ref_to_push = 'HEAD'
2821 parent = '%s/%s' % (gerrit_remote, branch)
2822 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2823
2824 assert change_desc
2825 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2826 ref_to_push)]).splitlines()
2827 if len(commits) > 1:
2828 print('WARNING: This will upload %d commits. Run the following command '
2829 'to see which commits will be uploaded: ' % len(commits))
2830 print('git log %s..%s' % (parent, ref_to_push))
2831 print('You can also use `git squash-branch` to squash these into a '
2832 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002833 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002834
2835 if options.reviewers or options.tbr_owners:
2836 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2837 change)
2838
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002839 # Extra options that can be specified at push time. Doc:
2840 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2841 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002842 if change_desc.get_reviewers(tbr_only=True):
2843 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2844 refspec_opts.append('l=Code-Review+1')
2845
Aaron Gable9b713dd2016-12-14 16:04:21 -08002846 if title:
2847 if not re.match(r'^[\w ]+$', title):
2848 title = re.sub(r'[^\w ]', '', title)
Aaron Gable856585d2017-01-23 15:32:00 -08002849 if not automatic_title:
2850 print('WARNING: Patchset title may only contain alphanumeric chars '
Aaron Gableeb3af712017-02-02 12:42:02 -08002851 'and spaces. You can edit it in the UI. '
2852 'See https://crbug.com/663787.\n'
2853 'Cleaned up title: %s' % title)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002854 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2855 # reverse on its side.
Aaron Gable9b713dd2016-12-14 16:04:21 -08002856 refspec_opts.append('m=' + title.replace(' ', '_'))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002857
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002858 if options.send_mail:
2859 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002860 DieWithError('Must specify reviewers to send email.', change_desc)
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002861 refspec_opts.append('notify=ALL')
2862 else:
2863 refspec_opts.append('notify=NONE')
2864
tandrii99a72f22016-08-17 14:33:24 -07002865 reviewers = change_desc.get_reviewers()
2866 if reviewers:
Andrii Shyshkalovaf3a9992017-02-09 14:18:01 +01002867 # TODO(tandrii): remove this horrible hack once (Poly)Gerrit fixes their
2868 # side for real (b/34702620).
2869 def clean_invisible_chars(email):
2870 return email.decode('unicode_escape').encode('ascii', 'ignore')
2871 refspec_opts.extend('r=' + clean_invisible_chars(email).strip()
2872 for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002873
agablec6787972016-09-09 16:13:34 -07002874 if options.private:
2875 refspec_opts.append('draft')
2876
rmistry9eadede2016-09-19 11:22:43 -07002877 if options.topic:
2878 # Documentation on Gerrit topics is here:
2879 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2880 refspec_opts.append('topic=%s' % options.topic)
2881
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002882 refspec_suffix = ''
2883 if refspec_opts:
2884 refspec_suffix = '%' + ','.join(refspec_opts)
2885 assert ' ' not in refspec_suffix, (
2886 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002887 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002888
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002889 try:
2890 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalove9c78ff2017-02-06 15:53:13 +01002891 ['git', 'push', self.GetRemoteUrl(), refspec],
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002892 print_stdout=True,
2893 # Flush after every line: useful for seeing progress when running as
2894 # recipe.
2895 filter_fn=lambda _: sys.stdout.flush())
2896 except subprocess2.CalledProcessError:
2897 DieWithError('Failed to create a change. Please examine output above '
Christopher Lamf732cd52017-01-24 12:40:11 +11002898 'for the reason of the failure. ', change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002899
2900 if options.squash:
2901 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2902 change_numbers = [m.group(1)
2903 for m in map(regex.match, push_stdout.splitlines())
2904 if m]
2905 if len(change_numbers) != 1:
2906 DieWithError(
2907 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002908 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002909 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002910 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002911
2912 # Add cc's from the CC_LIST and --cc flag (if any).
2913 cc = self.GetCCList().split(',')
2914 if options.cc:
2915 cc.extend(options.cc)
2916 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002917 if change_desc.get_cced():
2918 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002919 if cc:
Aaron Gable5db82092016-11-17 10:52:08 -08002920 gerrit_util.AddReviewers(
Aaron Gable59f48512017-01-12 10:54:46 -08002921 self._GetGerritHost(), self.GetIssue(), cc,
2922 is_reviewer=False, notify=bool(options.send_mail))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002923 return 0
2924
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002925 def _AddChangeIdToCommitMessage(self, options, args):
2926 """Re-commits using the current message, assumes the commit hook is in
2927 place.
2928 """
2929 log_desc = options.message or CreateDescriptionFromLog(args)
2930 git_command = ['commit', '--amend', '-m', log_desc]
2931 RunGit(git_command)
2932 new_log_desc = CreateDescriptionFromLog(args)
2933 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002934 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002935 return new_log_desc
2936 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002937 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002938
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002939 def SetCQState(self, new_state):
2940 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002941 vote_map = {
2942 _CQState.NONE: 0,
2943 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002944 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002945 }
Andrii Shyshkalov828701b2016-12-09 10:46:47 +01002946 kwargs = {'labels': {'Commit-Queue': vote_map[new_state]}}
2947 if new_state == _CQState.DRY_RUN:
2948 # Don't spam everybody reviewer/owner.
2949 kwargs['notify'] = 'NONE'
2950 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(), **kwargs)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002951
tandriie113dfd2016-10-11 10:20:12 -07002952 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002953 try:
2954 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002955 except GerritChangeNotExists:
2956 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002957
2958 if data['status'] in ('ABANDONED', 'MERGED'):
2959 return 'CL %s is closed' % self.GetIssue()
2960
2961 def GetTryjobProperties(self, patchset=None):
2962 """Returns dictionary of properties to launch tryjob."""
2963 data = self._GetChangeDetail(['ALL_REVISIONS'])
2964 patchset = int(patchset or self.GetPatchset())
2965 assert patchset
2966 revision_data = None # Pylint wants it to be defined.
2967 for revision_data in data['revisions'].itervalues():
2968 if int(revision_data['_number']) == patchset:
2969 break
2970 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002971 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002972 (patchset, self.GetIssue()))
2973 return {
2974 'patch_issue': self.GetIssue(),
2975 'patch_set': patchset or self.GetPatchset(),
2976 'patch_project': data['project'],
2977 'patch_storage': 'gerrit',
2978 'patch_ref': revision_data['fetch']['http']['ref'],
2979 'patch_repository_url': revision_data['fetch']['http']['url'],
2980 'patch_gerrit_url': self.GetCodereviewServer(),
2981 }
tandriie113dfd2016-10-11 10:20:12 -07002982
tandriide281ae2016-10-12 06:02:30 -07002983 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002984 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002985
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002986
2987_CODEREVIEW_IMPLEMENTATIONS = {
2988 'rietveld': _RietveldChangelistImpl,
2989 'gerrit': _GerritChangelistImpl,
2990}
2991
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002992
iannuccie53c9352016-08-17 14:40:40 -07002993def _add_codereview_issue_select_options(parser, extra=""):
2994 _add_codereview_select_options(parser)
2995
2996 text = ('Operate on this issue number instead of the current branch\'s '
2997 'implicit issue.')
2998 if extra:
2999 text += ' '+extra
3000 parser.add_option('-i', '--issue', type=int, help=text)
3001
3002
3003def _process_codereview_issue_select_options(parser, options):
3004 _process_codereview_select_options(parser, options)
3005 if options.issue is not None and not options.forced_codereview:
3006 parser.error('--issue must be specified with either --rietveld or --gerrit')
3007
3008
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003009def _add_codereview_select_options(parser):
3010 """Appends --gerrit and --rietveld options to force specific codereview."""
3011 parser.codereview_group = optparse.OptionGroup(
3012 parser, 'EXPERIMENTAL! Codereview override options')
3013 parser.add_option_group(parser.codereview_group)
3014 parser.codereview_group.add_option(
3015 '--gerrit', action='store_true',
3016 help='Force the use of Gerrit for codereview')
3017 parser.codereview_group.add_option(
3018 '--rietveld', action='store_true',
3019 help='Force the use of Rietveld for codereview')
3020
3021
3022def _process_codereview_select_options(parser, options):
3023 if options.gerrit and options.rietveld:
3024 parser.error('Options --gerrit and --rietveld are mutually exclusive')
3025 options.forced_codereview = None
3026 if options.gerrit:
3027 options.forced_codereview = 'gerrit'
3028 elif options.rietveld:
3029 options.forced_codereview = 'rietveld'
3030
3031
tandriif9aefb72016-07-01 09:06:51 -07003032def _get_bug_line_values(default_project, bugs):
3033 """Given default_project and comma separated list of bugs, yields bug line
3034 values.
3035
3036 Each bug can be either:
3037 * a number, which is combined with default_project
3038 * string, which is left as is.
3039
3040 This function may produce more than one line, because bugdroid expects one
3041 project per line.
3042
3043 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3044 ['v8:123', 'chromium:789']
3045 """
3046 default_bugs = []
3047 others = []
3048 for bug in bugs.split(','):
3049 bug = bug.strip()
3050 if bug:
3051 try:
3052 default_bugs.append(int(bug))
3053 except ValueError:
3054 others.append(bug)
3055
3056 if default_bugs:
3057 default_bugs = ','.join(map(str, default_bugs))
3058 if default_project:
3059 yield '%s:%s' % (default_project, default_bugs)
3060 else:
3061 yield default_bugs
3062 for other in sorted(others):
3063 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3064 yield other
3065
3066
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003067class ChangeDescription(object):
3068 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003069 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003070 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Mark Mentovai600d3092017-03-08 12:58:18 -05003071 BUG_LINE = r'^[ \t]*(BUGS?|Bugs?)[ \t]*[:=][ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003072 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003073
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003074 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003075 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003076
agable@chromium.org42c20792013-09-12 17:34:49 +00003077 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003078 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003079 return '\n'.join(self._description_lines)
3080
3081 def set_description(self, desc):
3082 if isinstance(desc, basestring):
3083 lines = desc.splitlines()
3084 else:
3085 lines = [line.rstrip() for line in desc]
3086 while lines and not lines[0]:
3087 lines.pop(0)
3088 while lines and not lines[-1]:
3089 lines.pop(-1)
3090 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003091
piman@chromium.org336f9122014-09-04 02:16:55 +00003092 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003093 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003094 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00003095 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003096 return
agable@chromium.org42c20792013-09-12 17:34:49 +00003097 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003098
agable@chromium.org42c20792013-09-12 17:34:49 +00003099 # Get the set of R= and TBR= lines and remove them from the desciption.
3100 regexp = re.compile(self.R_LINE)
3101 matches = [regexp.match(line) for line in self._description_lines]
3102 new_desc = [l for i, l in enumerate(self._description_lines)
3103 if not matches[i]]
3104 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003105
agable@chromium.org42c20792013-09-12 17:34:49 +00003106 # Construct new unified R= and TBR= lines.
3107 r_names = []
3108 tbr_names = []
3109 for match in matches:
3110 if not match:
3111 continue
3112 people = cleanup_list([match.group(2).strip()])
3113 if match.group(1) == 'TBR':
3114 tbr_names.extend(people)
3115 else:
3116 r_names.extend(people)
3117 for name in r_names:
3118 if name not in reviewers:
3119 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003120 if add_owners_tbr:
3121 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003122 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003123 all_reviewers = set(tbr_names + reviewers)
3124 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3125 all_reviewers)
3126 tbr_names.extend(owners_db.reviewers_for(missing_files,
3127 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003128 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3129 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3130
3131 # Put the new lines in the description where the old first R= line was.
3132 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3133 if 0 <= line_loc < len(self._description_lines):
3134 if new_tbr_line:
3135 self._description_lines.insert(line_loc, new_tbr_line)
3136 if new_r_line:
3137 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003138 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003139 if new_r_line:
3140 self.append_footer(new_r_line)
3141 if new_tbr_line:
3142 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003143
tandriif9aefb72016-07-01 09:06:51 -07003144 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003145 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003146 self.set_description([
3147 '# Enter a description of the change.',
3148 '# This will be displayed on the codereview site.',
3149 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003150 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003151 '--------------------',
3152 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003153
agable@chromium.org42c20792013-09-12 17:34:49 +00003154 regexp = re.compile(self.BUG_LINE)
3155 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003156 prefix = settings.GetBugPrefix()
3157 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Mark Mentovai57c47212017-03-09 11:14:09 -05003158 bug_line_format = settings.GetBugLineFormat()
tandriif9aefb72016-07-01 09:06:51 -07003159 for value in values:
Mark Mentovai57c47212017-03-09 11:14:09 -05003160 self.append_footer(bug_line_format % value)
tandriif9aefb72016-07-01 09:06:51 -07003161
agable@chromium.org42c20792013-09-12 17:34:49 +00003162 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003163 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003164 if not content:
3165 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003166 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003167
3168 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003169 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3170 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003171 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003172 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003173
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003174 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003175 """Adds a footer line to the description.
3176
3177 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3178 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3179 that Gerrit footers are always at the end.
3180 """
3181 parsed_footer_line = git_footers.parse_footer(line)
3182 if parsed_footer_line:
3183 # Line is a gerrit footer in the form: Footer-Key: any value.
3184 # Thus, must be appended observing Gerrit footer rules.
3185 self.set_description(
3186 git_footers.add_footer(self.description,
3187 key=parsed_footer_line[0],
3188 value=parsed_footer_line[1]))
3189 return
3190
3191 if not self._description_lines:
3192 self._description_lines.append(line)
3193 return
3194
3195 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3196 if gerrit_footers:
3197 # git_footers.split_footers ensures that there is an empty line before
3198 # actual (gerrit) footers, if any. We have to keep it that way.
3199 assert top_lines and top_lines[-1] == ''
3200 top_lines, separator = top_lines[:-1], top_lines[-1:]
3201 else:
3202 separator = [] # No need for separator if there are no gerrit_footers.
3203
3204 prev_line = top_lines[-1] if top_lines else ''
3205 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3206 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3207 top_lines.append('')
3208 top_lines.append(line)
3209 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003210
tandrii99a72f22016-08-17 14:33:24 -07003211 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003212 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003213 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003214 reviewers = [match.group(2).strip()
3215 for match in matches
3216 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003217 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003218
bradnelsond975b302016-10-23 12:20:23 -07003219 def get_cced(self):
3220 """Retrieves the list of reviewers."""
3221 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3222 cced = [match.group(2).strip() for match in matches if match]
3223 return cleanup_list(cced)
3224
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003225 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3226 """Updates this commit description given the parent.
3227
3228 This is essentially what Gnumbd used to do.
3229 Consult https://goo.gl/WMmpDe for more details.
3230 """
3231 assert parent_msg # No, orphan branch creation isn't supported.
3232 assert parent_hash
3233 assert dest_ref
3234 parent_footer_map = git_footers.parse_footers(parent_msg)
3235 # This will also happily parse svn-position, which GnumbD is no longer
3236 # supporting. While we'd generate correct footers, the verifier plugin
3237 # installed in Gerrit will block such commit (ie git push below will fail).
3238 parent_position = git_footers.get_position(parent_footer_map)
3239
3240 # Cherry-picks may have last line obscuring their prior footers,
3241 # from git_footers perspective. This is also what Gnumbd did.
3242 cp_line = None
3243 if (self._description_lines and
3244 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3245 cp_line = self._description_lines.pop()
3246
3247 top_lines, _, parsed_footers = git_footers.split_footers(self.description)
3248
3249 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3250 # user interference with actual footers we'd insert below.
3251 for i, (k, v) in enumerate(parsed_footers):
3252 if k.startswith('Cr-'):
3253 parsed_footers[i] = (k.replace('Cr-', 'Cr-Original-'), v)
3254
3255 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003256 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003257 if parent_position[0] == dest_ref:
3258 # Same branch as parent.
3259 number = int(parent_position[1]) + 1
3260 else:
3261 number = 1 # New branch, and extra lineage.
3262 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3263 int(parent_position[1])))
3264
3265 parsed_footers.append(('Cr-Commit-Position',
3266 '%s@{#%d}' % (dest_ref, number)))
3267 parsed_footers.extend(('Cr-Branched-From', v) for v in lineage)
3268
3269 self._description_lines = top_lines
3270 if cp_line:
3271 self._description_lines.append(cp_line)
3272 if self._description_lines[-1] != '':
3273 self._description_lines.append('') # Ensure footer separator.
3274 self._description_lines.extend('%s: %s' % kv for kv in parsed_footers)
3275
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003276
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003277def get_approving_reviewers(props):
3278 """Retrieves the reviewers that approved a CL from the issue properties with
3279 messages.
3280
3281 Note that the list may contain reviewers that are not committer, thus are not
3282 considered by the CQ.
3283 """
3284 return sorted(
3285 set(
3286 message['sender']
3287 for message in props['messages']
3288 if message['approval'] and message['sender'] in props['reviewers']
3289 )
3290 )
3291
3292
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003293def FindCodereviewSettingsFile(filename='codereview.settings'):
3294 """Finds the given file starting in the cwd and going up.
3295
3296 Only looks up to the top of the repository unless an
3297 'inherit-review-settings-ok' file exists in the root of the repository.
3298 """
3299 inherit_ok_file = 'inherit-review-settings-ok'
3300 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003301 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003302 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3303 root = '/'
3304 while True:
3305 if filename in os.listdir(cwd):
3306 if os.path.isfile(os.path.join(cwd, filename)):
3307 return open(os.path.join(cwd, filename))
3308 if cwd == root:
3309 break
3310 cwd = os.path.dirname(cwd)
3311
3312
3313def LoadCodereviewSettingsFromFile(fileobj):
3314 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003315 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003316
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003317 def SetProperty(name, setting, unset_error_ok=False):
3318 fullname = 'rietveld.' + name
3319 if setting in keyvals:
3320 RunGit(['config', fullname, keyvals[setting]])
3321 else:
3322 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3323
tandrii48df5812016-10-17 03:55:37 -07003324 if not keyvals.get('GERRIT_HOST', False):
3325 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003326 # Only server setting is required. Other settings can be absent.
3327 # In that case, we ignore errors raised during option deletion attempt.
3328 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003329 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003330 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3331 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
Mark Mentovai57c47212017-03-09 11:14:09 -05003332 SetProperty('bug-line-format', 'BUG_LINE_FORMAT', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003333 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003334 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3335 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003336 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003337 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3338 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003339
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003340 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003341 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003342
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003343 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003344 RunGit(['config', 'gerrit.squash-uploads',
3345 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003346
tandrii@chromium.org28253532016-04-14 13:46:56 +00003347 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003348 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003349 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3350
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003351 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003352 # should be of the form
3353 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3354 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003355 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3356 keyvals['ORIGIN_URL_CONFIG']])
3357
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003358
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003359def urlretrieve(source, destination):
3360 """urllib is broken for SSL connections via a proxy therefore we
3361 can't use urllib.urlretrieve()."""
3362 with open(destination, 'w') as f:
3363 f.write(urllib2.urlopen(source).read())
3364
3365
ukai@chromium.org712d6102013-11-27 00:52:58 +00003366def hasSheBang(fname):
3367 """Checks fname is a #! script."""
3368 with open(fname) as f:
3369 return f.read(2).startswith('#!')
3370
3371
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003372# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3373def DownloadHooks(*args, **kwargs):
3374 pass
3375
3376
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003377def DownloadGerritHook(force):
3378 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003379
3380 Args:
3381 force: True to update hooks. False to install hooks if not present.
3382 """
3383 if not settings.GetIsGerrit():
3384 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003385 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003386 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3387 if not os.access(dst, os.X_OK):
3388 if os.path.exists(dst):
3389 if not force:
3390 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003391 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003392 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003393 if not hasSheBang(dst):
3394 DieWithError('Not a script: %s\n'
3395 'You need to download from\n%s\n'
3396 'into .git/hooks/commit-msg and '
3397 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003398 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3399 except Exception:
3400 if os.path.exists(dst):
3401 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003402 DieWithError('\nFailed to download hooks.\n'
3403 'You need to download from\n%s\n'
3404 'into .git/hooks/commit-msg and '
3405 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003406
3407
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003408def GetRietveldCodereviewSettingsInteractively():
3409 """Prompt the user for settings."""
3410 server = settings.GetDefaultServerUrl(error_ok=True)
3411 prompt = 'Rietveld server (host[:port])'
3412 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3413 newserver = ask_for_data(prompt + ':')
3414 if not server and not newserver:
3415 newserver = DEFAULT_SERVER
3416 if newserver:
3417 newserver = gclient_utils.UpgradeToHttps(newserver)
3418 if newserver != server:
3419 RunGit(['config', 'rietveld.server', newserver])
3420
3421 def SetProperty(initial, caption, name, is_url):
3422 prompt = caption
3423 if initial:
3424 prompt += ' ("x" to clear) [%s]' % initial
3425 new_val = ask_for_data(prompt + ':')
3426 if new_val == 'x':
3427 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3428 elif new_val:
3429 if is_url:
3430 new_val = gclient_utils.UpgradeToHttps(new_val)
3431 if new_val != initial:
3432 RunGit(['config', 'rietveld.' + name, new_val])
3433
3434 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3435 SetProperty(settings.GetDefaultPrivateFlag(),
3436 'Private flag (rietveld only)', 'private', False)
3437 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3438 'tree-status-url', False)
3439 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3440 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3441 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3442 'run-post-upload-hook', False)
3443
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003444
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003445@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003446def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003447 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003448
tandrii5d0a0422016-09-14 06:24:35 -07003449 print('WARNING: git cl config works for Rietveld only')
3450 # TODO(tandrii): remove this once we switch to Gerrit.
3451 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003452 parser.add_option('--activate-update', action='store_true',
3453 help='activate auto-updating [rietveld] section in '
3454 '.git/config')
3455 parser.add_option('--deactivate-update', action='store_true',
3456 help='deactivate auto-updating [rietveld] section in '
3457 '.git/config')
3458 options, args = parser.parse_args(args)
3459
3460 if options.deactivate_update:
3461 RunGit(['config', 'rietveld.autoupdate', 'false'])
3462 return
3463
3464 if options.activate_update:
3465 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3466 return
3467
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003468 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003469 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003470 return 0
3471
3472 url = args[0]
3473 if not url.endswith('codereview.settings'):
3474 url = os.path.join(url, 'codereview.settings')
3475
3476 # Load code review settings and download hooks (if available).
3477 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3478 return 0
3479
3480
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003481def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003482 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003483 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3484 branch = ShortBranchName(branchref)
3485 _, args = parser.parse_args(args)
3486 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003487 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003488 return RunGit(['config', 'branch.%s.base-url' % branch],
3489 error_ok=False).strip()
3490 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003491 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003492 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3493 error_ok=False).strip()
3494
3495
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003496def color_for_status(status):
3497 """Maps a Changelist status to color, for CMDstatus and other tools."""
3498 return {
3499 'unsent': Fore.RED,
3500 'waiting': Fore.BLUE,
3501 'reply': Fore.YELLOW,
3502 'lgtm': Fore.GREEN,
3503 'commit': Fore.MAGENTA,
3504 'closed': Fore.CYAN,
3505 'error': Fore.WHITE,
3506 }.get(status, Fore.WHITE)
3507
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003508
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003509def get_cl_statuses(changes, fine_grained, max_processes=None):
3510 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003511
3512 If fine_grained is true, this will fetch CL statuses from the server.
3513 Otherwise, simply indicate if there's a matching url for the given branches.
3514
3515 If max_processes is specified, it is used as the maximum number of processes
3516 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3517 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003518
3519 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003520 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003521 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003522 upload.verbosity = 0
3523
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003524 if not changes:
3525 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003526
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003527 if not fine_grained:
3528 # Fast path which doesn't involve querying codereview servers.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003529 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003530 for cl in changes:
3531 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003532 return
3533
3534 # First, sort out authentication issues.
3535 logging.debug('ensuring credentials exist')
3536 for cl in changes:
3537 cl.EnsureAuthenticated(force=False, refresh=True)
3538
3539 def fetch(cl):
3540 try:
3541 return (cl, cl.GetStatus())
3542 except:
3543 # See http://crbug.com/629863.
3544 logging.exception('failed to fetch status for %s:', cl)
3545 raise
3546
3547 threads_count = len(changes)
3548 if max_processes:
3549 threads_count = max(1, min(threads_count, max_processes))
3550 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3551
3552 pool = ThreadPool(threads_count)
3553 fetched_cls = set()
3554 try:
3555 it = pool.imap_unordered(fetch, changes).__iter__()
3556 while True:
3557 try:
3558 cl, status = it.next(timeout=5)
3559 except multiprocessing.TimeoutError:
3560 break
3561 fetched_cls.add(cl)
3562 yield cl, status
3563 finally:
3564 pool.close()
3565
3566 # Add any branches that failed to fetch.
3567 for cl in set(changes) - fetched_cls:
3568 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003569
rmistry@google.com2dd99862015-06-22 12:22:18 +00003570
3571def upload_branch_deps(cl, args):
3572 """Uploads CLs of local branches that are dependents of the current branch.
3573
3574 If the local branch dependency tree looks like:
3575 test1 -> test2.1 -> test3.1
3576 -> test3.2
3577 -> test2.2 -> test3.3
3578
3579 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3580 run on the dependent branches in this order:
3581 test2.1, test3.1, test3.2, test2.2, test3.3
3582
3583 Note: This function does not rebase your local dependent branches. Use it when
3584 you make a change to the parent branch that will not conflict with its
3585 dependent branches, and you would like their dependencies updated in
3586 Rietveld.
3587 """
3588 if git_common.is_dirty_git_tree('upload-branch-deps'):
3589 return 1
3590
3591 root_branch = cl.GetBranch()
3592 if root_branch is None:
3593 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3594 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01003595 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00003596 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3597 'patchset dependencies without an uploaded CL.')
3598
3599 branches = RunGit(['for-each-ref',
3600 '--format=%(refname:short) %(upstream:short)',
3601 'refs/heads'])
3602 if not branches:
3603 print('No local branches found.')
3604 return 0
3605
3606 # Create a dictionary of all local branches to the branches that are dependent
3607 # on it.
3608 tracked_to_dependents = collections.defaultdict(list)
3609 for b in branches.splitlines():
3610 tokens = b.split()
3611 if len(tokens) == 2:
3612 branch_name, tracked = tokens
3613 tracked_to_dependents[tracked].append(branch_name)
3614
vapiera7fbd5a2016-06-16 09:17:49 -07003615 print()
3616 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003617 dependents = []
3618 def traverse_dependents_preorder(branch, padding=''):
3619 dependents_to_process = tracked_to_dependents.get(branch, [])
3620 padding += ' '
3621 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003622 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003623 dependents.append(dependent)
3624 traverse_dependents_preorder(dependent, padding)
3625 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003626 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003627
3628 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003629 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003630 return 0
3631
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003632 confirm_or_exit('This command will checkout all dependent branches and run '
3633 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003634
andybons@chromium.org962f9462016-02-03 20:00:42 +00003635 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003636 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003637 args.extend(['-t', 'Updated patchset dependency'])
3638
rmistry@google.com2dd99862015-06-22 12:22:18 +00003639 # Record all dependents that failed to upload.
3640 failures = {}
3641 # Go through all dependents, checkout the branch and upload.
3642 try:
3643 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003644 print()
3645 print('--------------------------------------')
3646 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003647 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003648 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003649 try:
3650 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003651 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003652 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003653 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003654 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003655 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003656 finally:
3657 # Swap back to the original root branch.
3658 RunGit(['checkout', '-q', root_branch])
3659
vapiera7fbd5a2016-06-16 09:17:49 -07003660 print()
3661 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003662 for dependent_branch in dependents:
3663 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003664 print(' %s : %s' % (dependent_branch, upload_status))
3665 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003666
3667 return 0
3668
3669
kmarshall3bff56b2016-06-06 18:31:47 -07003670def CMDarchive(parser, args):
3671 """Archives and deletes branches associated with closed changelists."""
3672 parser.add_option(
3673 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003674 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003675 parser.add_option(
3676 '-f', '--force', action='store_true',
3677 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003678 parser.add_option(
3679 '-d', '--dry-run', action='store_true',
3680 help='Skip the branch tagging and removal steps.')
3681 parser.add_option(
3682 '-t', '--notags', action='store_true',
3683 help='Do not tag archived branches. '
3684 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003685
3686 auth.add_auth_options(parser)
3687 options, args = parser.parse_args(args)
3688 if args:
3689 parser.error('Unsupported args: %s' % ' '.join(args))
3690 auth_config = auth.extract_auth_config_from_options(options)
3691
3692 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3693 if not branches:
3694 return 0
3695
vapiera7fbd5a2016-06-16 09:17:49 -07003696 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003697 changes = [Changelist(branchref=b, auth_config=auth_config)
3698 for b in branches.splitlines()]
3699 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3700 statuses = get_cl_statuses(changes,
3701 fine_grained=True,
3702 max_processes=options.maxjobs)
3703 proposal = [(cl.GetBranch(),
3704 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3705 for cl, status in statuses
3706 if status == 'closed']
3707 proposal.sort()
3708
3709 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003710 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003711 return 0
3712
3713 current_branch = GetCurrentBranch()
3714
vapiera7fbd5a2016-06-16 09:17:49 -07003715 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003716 if options.notags:
3717 for next_item in proposal:
3718 print(' ' + next_item[0])
3719 else:
3720 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3721 for next_item in proposal:
3722 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003723
kmarshall9249e012016-08-23 12:02:16 -07003724 # Quit now on precondition failure or if instructed by the user, either
3725 # via an interactive prompt or by command line flags.
3726 if options.dry_run:
3727 print('\nNo changes were made (dry run).\n')
3728 return 0
3729 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003730 print('You are currently on a branch \'%s\' which is associated with a '
3731 'closed codereview issue, so archive cannot proceed. Please '
3732 'checkout another branch and run this command again.' %
3733 current_branch)
3734 return 1
kmarshall9249e012016-08-23 12:02:16 -07003735 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003736 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3737 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003738 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003739 return 1
3740
3741 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003742 if not options.notags:
3743 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003744 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003745
vapiera7fbd5a2016-06-16 09:17:49 -07003746 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003747
3748 return 0
3749
3750
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003751def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003752 """Show status of changelists.
3753
3754 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003755 - Red not sent for review or broken
3756 - Blue waiting for review
3757 - Yellow waiting for you to reply to review
3758 - Green LGTM'ed
3759 - Magenta in the commit queue
3760 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003761
3762 Also see 'git cl comments'.
3763 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003764 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003765 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003766 parser.add_option('-f', '--fast', action='store_true',
3767 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003768 parser.add_option(
3769 '-j', '--maxjobs', action='store', type=int,
3770 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003771
3772 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003773 _add_codereview_issue_select_options(
3774 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003775 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003776 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003777 if args:
3778 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003779 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003780
iannuccie53c9352016-08-17 14:40:40 -07003781 if options.issue is not None and not options.field:
3782 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003783
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003784 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003785 cl = Changelist(auth_config=auth_config, issue=options.issue,
3786 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003787 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003788 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003789 elif options.field == 'id':
3790 issueid = cl.GetIssue()
3791 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003792 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003793 elif options.field == 'patch':
3794 patchset = cl.GetPatchset()
3795 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003796 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003797 elif options.field == 'status':
3798 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003799 elif options.field == 'url':
3800 url = cl.GetIssueURL()
3801 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003802 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003803 return 0
3804
3805 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3806 if not branches:
3807 print('No local branch found.')
3808 return 0
3809
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003810 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003811 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003812 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003813 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003814 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003815 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003816 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003817
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003818 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003819 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3820 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3821 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003822 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003823 c, status = output.next()
3824 branch_statuses[c.GetBranch()] = status
3825 status = branch_statuses.pop(branch)
3826 url = cl.GetIssueURL()
3827 if url and (not status or status == 'error'):
3828 # The issue probably doesn't exist anymore.
3829 url += ' (broken)'
3830
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003831 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003832 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003833 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003834 color = ''
3835 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003836 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003837 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003838 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003839 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003840
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003841
3842 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07003843 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003844 print('Current branch: %s' % branch)
3845 for cl in changes:
3846 if cl.GetBranch() == branch:
3847 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003848 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003849 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003850 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003851 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003852 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003853 print('Issue description:')
3854 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003855 return 0
3856
3857
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003858def colorize_CMDstatus_doc():
3859 """To be called once in main() to add colors to git cl status help."""
3860 colors = [i for i in dir(Fore) if i[0].isupper()]
3861
3862 def colorize_line(line):
3863 for color in colors:
3864 if color in line.upper():
3865 # Extract whitespaces first and the leading '-'.
3866 indent = len(line) - len(line.lstrip(' ')) + 1
3867 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3868 return line
3869
3870 lines = CMDstatus.__doc__.splitlines()
3871 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3872
3873
phajdan.jre328cf92016-08-22 04:12:17 -07003874def write_json(path, contents):
3875 with open(path, 'w') as f:
3876 json.dump(contents, f)
3877
3878
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003879@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003880def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003881 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003882
3883 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003884 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003885 parser.add_option('-r', '--reverse', action='store_true',
3886 help='Lookup the branch(es) for the specified issues. If '
3887 'no issues are specified, all branches with mapped '
3888 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003889 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003890 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003891 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003892 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003893
dnj@chromium.org406c4402015-03-03 17:22:28 +00003894 if options.reverse:
3895 branches = RunGit(['for-each-ref', 'refs/heads',
3896 '--format=%(refname:short)']).splitlines()
3897
3898 # Reverse issue lookup.
3899 issue_branch_map = {}
3900 for branch in branches:
3901 cl = Changelist(branchref=branch)
3902 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3903 if not args:
3904 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003905 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003906 for issue in args:
3907 if not issue:
3908 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003909 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003910 print('Branch for issue number %s: %s' % (
3911 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003912 if options.json:
3913 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003914 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003915 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003916 if len(args) > 0:
3917 try:
3918 issue = int(args[0])
3919 except ValueError:
3920 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003921 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003922 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003923 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003924 if options.json:
3925 write_json(options.json, {
3926 'issue': cl.GetIssue(),
3927 'issue_url': cl.GetIssueURL(),
3928 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003929 return 0
3930
3931
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003932def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003933 """Shows or posts review comments for any changelist."""
3934 parser.add_option('-a', '--add-comment', dest='comment',
3935 help='comment to add to an issue')
3936 parser.add_option('-i', dest='issue',
3937 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003938 parser.add_option('-j', '--json-file',
3939 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003940 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003941 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003942 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003943
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003944 issue = None
3945 if options.issue:
3946 try:
3947 issue = int(options.issue)
3948 except ValueError:
3949 DieWithError('A review issue id is expected to be a number')
3950
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003951 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003952
3953 if options.comment:
3954 cl.AddComment(options.comment)
3955 return 0
3956
3957 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003958 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003959 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003960 summary.append({
3961 'date': message['date'],
3962 'lgtm': False,
3963 'message': message['text'],
3964 'not_lgtm': False,
3965 'sender': message['sender'],
3966 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003967 if message['disapproval']:
3968 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003969 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003970 elif message['approval']:
3971 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003972 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003973 elif message['sender'] == data['owner_email']:
3974 color = Fore.MAGENTA
3975 else:
3976 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003977 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003978 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003979 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003980 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003981 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003982 if options.json_file:
3983 with open(options.json_file, 'wb') as f:
3984 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003985 return 0
3986
3987
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003988@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003989def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003990 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003991 parser.add_option('-d', '--display', action='store_true',
3992 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003993 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003994 help='New description to set for this issue (- for stdin, '
3995 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003996 parser.add_option('-f', '--force', action='store_true',
3997 help='Delete any unpublished Gerrit edits for this issue '
3998 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003999
4000 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004001 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004002 options, args = parser.parse_args(args)
4003 _process_codereview_select_options(parser, options)
4004
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004005 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004006 if len(args) > 0:
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004007 target_issue_arg = ParseIssueNumberArgument(args[0])
4008 if not target_issue_arg.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004009 parser.print_help()
4010 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004011
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004012 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004013
martiniss6eda05f2016-06-30 10:18:35 -07004014 kwargs = {
4015 'auth_config': auth_config,
4016 'codereview': options.forced_codereview,
4017 }
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004018 if target_issue_arg:
4019 kwargs['issue'] = target_issue_arg.issue
4020 kwargs['codereview_host'] = target_issue_arg.hostname
martiniss6eda05f2016-06-30 10:18:35 -07004021
4022 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004023
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004024 if not cl.GetIssue():
4025 DieWithError('This branch has no associated changelist.')
4026 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004027
smut@google.com34fb6b12015-07-13 20:03:26 +00004028 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004029 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004030 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004031
4032 if options.new_description:
4033 text = options.new_description
4034 if text == '-':
4035 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004036 elif text == '+':
4037 base_branch = cl.GetCommonAncestorWithUpstream()
4038 change = cl.GetChange(base_branch, None, local_description=True)
4039 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004040
4041 description.set_description(text)
4042 else:
4043 description.prompt()
4044
wychen@chromium.org063e4e52015-04-03 06:51:44 +00004045 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004046 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004047 return 0
4048
4049
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004050def CreateDescriptionFromLog(args):
4051 """Pulls out the commit log to use as a base for the CL description."""
4052 log_args = []
4053 if len(args) == 1 and not args[0].endswith('.'):
4054 log_args = [args[0] + '..']
4055 elif len(args) == 1 and args[0].endswith('...'):
4056 log_args = [args[0][:-1]]
4057 elif len(args) == 2:
4058 log_args = [args[0] + '..' + args[1]]
4059 else:
4060 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004061 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004062
4063
thestig@chromium.org44202a22014-03-11 19:22:18 +00004064def CMDlint(parser, args):
4065 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004066 parser.add_option('--filter', action='append', metavar='-x,+y',
4067 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004068 auth.add_auth_options(parser)
4069 options, args = parser.parse_args(args)
4070 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004071
4072 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004073 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004074 try:
4075 import cpplint
4076 import cpplint_chromium
4077 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004078 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004079 return 1
4080
4081 # Change the current working directory before calling lint so that it
4082 # shows the correct base.
4083 previous_cwd = os.getcwd()
4084 os.chdir(settings.GetRoot())
4085 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004086 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004087 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4088 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004089 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004090 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004091 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004092
4093 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004094 command = args + files
4095 if options.filter:
4096 command = ['--filter=' + ','.join(options.filter)] + command
4097 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004098
4099 white_regex = re.compile(settings.GetLintRegex())
4100 black_regex = re.compile(settings.GetLintIgnoreRegex())
4101 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4102 for filename in filenames:
4103 if white_regex.match(filename):
4104 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004105 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004106 else:
4107 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4108 extra_check_functions)
4109 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004110 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004111 finally:
4112 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004113 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004114 if cpplint._cpplint_state.error_count != 0:
4115 return 1
4116 return 0
4117
4118
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004119def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004120 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004121 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004122 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004123 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004124 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004125 auth.add_auth_options(parser)
4126 options, args = parser.parse_args(args)
4127 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004128
sbc@chromium.org71437c02015-04-09 19:29:40 +00004129 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004130 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004131 return 1
4132
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004133 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004134 if args:
4135 base_branch = args[0]
4136 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004137 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004138 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004139
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004140 cl.RunHook(
4141 committing=not options.upload,
4142 may_prompt=False,
4143 verbose=options.verbose,
4144 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004145 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004146
4147
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004148def GenerateGerritChangeId(message):
4149 """Returns Ixxxxxx...xxx change id.
4150
4151 Works the same way as
4152 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4153 but can be called on demand on all platforms.
4154
4155 The basic idea is to generate git hash of a state of the tree, original commit
4156 message, author/committer info and timestamps.
4157 """
4158 lines = []
4159 tree_hash = RunGitSilent(['write-tree'])
4160 lines.append('tree %s' % tree_hash.strip())
4161 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4162 if code == 0:
4163 lines.append('parent %s' % parent.strip())
4164 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4165 lines.append('author %s' % author.strip())
4166 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4167 lines.append('committer %s' % committer.strip())
4168 lines.append('')
4169 # Note: Gerrit's commit-hook actually cleans message of some lines and
4170 # whitespace. This code is not doing this, but it clearly won't decrease
4171 # entropy.
4172 lines.append(message)
4173 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4174 stdin='\n'.join(lines))
4175 return 'I%s' % change_hash.strip()
4176
4177
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004178def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004179 """Computes the remote branch ref to use for the CL.
4180
4181 Args:
4182 remote (str): The git remote for the CL.
4183 remote_branch (str): The git remote branch for the CL.
4184 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004185 """
4186 if not (remote and remote_branch):
4187 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004188
wittman@chromium.org455dc922015-01-26 20:15:50 +00004189 if target_branch:
4190 # Cannonicalize branch references to the equivalent local full symbolic
4191 # refs, which are then translated into the remote full symbolic refs
4192 # below.
4193 if '/' not in target_branch:
4194 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4195 else:
4196 prefix_replacements = (
4197 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4198 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4199 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4200 )
4201 match = None
4202 for regex, replacement in prefix_replacements:
4203 match = re.search(regex, target_branch)
4204 if match:
4205 remote_branch = target_branch.replace(match.group(0), replacement)
4206 break
4207 if not match:
4208 # This is a branch path but not one we recognize; use as-is.
4209 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004210 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4211 # Handle the refs that need to land in different refs.
4212 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004213
wittman@chromium.org455dc922015-01-26 20:15:50 +00004214 # Create the true path to the remote branch.
4215 # Does the following translation:
4216 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4217 # * refs/remotes/origin/master -> refs/heads/master
4218 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4219 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4220 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4221 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4222 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4223 'refs/heads/')
4224 elif remote_branch.startswith('refs/remotes/branch-heads'):
4225 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004226
wittman@chromium.org455dc922015-01-26 20:15:50 +00004227 return remote_branch
4228
4229
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004230def cleanup_list(l):
4231 """Fixes a list so that comma separated items are put as individual items.
4232
4233 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4234 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4235 """
4236 items = sum((i.split(',') for i in l), [])
4237 stripped_items = (i.strip() for i in items)
4238 return sorted(filter(None, stripped_items))
4239
4240
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004241@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004242def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004243 """Uploads the current changelist to codereview.
4244
4245 Can skip dependency patchset uploads for a branch by running:
4246 git config branch.branch_name.skip-deps-uploads True
4247 To unset run:
4248 git config --unset branch.branch_name.skip-deps-uploads
4249 Can also set the above globally by using the --global flag.
4250 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004251 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4252 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004253 parser.add_option('--bypass-watchlists', action='store_true',
4254 dest='bypass_watchlists',
4255 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004256 parser.add_option('-f', action='store_true', dest='force',
4257 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004258 parser.add_option('--message', '-m', dest='message',
4259 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004260 parser.add_option('-b', '--bug',
4261 help='pre-populate the bug number(s) for this issue. '
4262 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004263 parser.add_option('--message-file', dest='message_file',
4264 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004265 parser.add_option('--title', '-t', dest='title',
4266 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004267 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004268 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004269 help='reviewer email addresses')
4270 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004271 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004272 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004273 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004274 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004275 parser.add_option('--emulate_svn_auto_props',
4276 '--emulate-svn-auto-props',
4277 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004278 dest="emulate_svn_auto_props",
4279 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004280 parser.add_option('-c', '--use-commit-queue', action='store_true',
4281 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004282 parser.add_option('--private', action='store_true',
4283 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004284 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004285 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004286 metavar='TARGET',
4287 help='Apply CL to remote ref TARGET. ' +
4288 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004289 parser.add_option('--squash', action='store_true',
4290 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004291 parser.add_option('--no-squash', action='store_true',
4292 help='Don\'t squash multiple commits into one ' +
4293 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004294 parser.add_option('--topic', default=None,
4295 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004296 parser.add_option('--email', default=None,
4297 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004298 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4299 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004300 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4301 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004302 help='Send the patchset to do a CQ dry run right after '
4303 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004304 parser.add_option('--dependencies', action='store_true',
4305 help='Uploads CLs of all the local branches that depend on '
4306 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004307
rmistry@google.com2dd99862015-06-22 12:22:18 +00004308 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004309 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004310 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004311 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004312 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004313 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004314 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004315
sbc@chromium.org71437c02015-04-09 19:29:40 +00004316 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004317 return 1
4318
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004319 options.reviewers = cleanup_list(options.reviewers)
4320 options.cc = cleanup_list(options.cc)
4321
tandriib80458a2016-06-23 12:20:07 -07004322 if options.message_file:
4323 if options.message:
4324 parser.error('only one of --message and --message-file allowed.')
4325 options.message = gclient_utils.FileRead(options.message_file)
4326 options.message_file = None
4327
tandrii4d0545a2016-07-06 03:56:49 -07004328 if options.cq_dry_run and options.use_commit_queue:
4329 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4330
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004331 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4332 settings.GetIsGerrit()
4333
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004334 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004335 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004336
4337
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004338@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004339def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004340 """DEPRECATED: Used to commit the current changelist via git-svn."""
4341 message = ('git-cl no longer supports committing to SVN repositories via '
4342 'git-svn. You probably want to use `git cl land` instead.')
4343 print(message)
4344 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004345
4346
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004347@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004348def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004349 """Commits the current changelist via git.
4350
4351 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4352 upstream and closes the issue automatically and atomically.
4353
4354 Otherwise (in case of Rietveld):
4355 Squashes branch into a single commit.
4356 Updates commit message with metadata (e.g. pointer to review).
4357 Pushes the code upstream.
4358 Updates review and closes.
4359 """
4360 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4361 help='bypass upload presubmit hook')
4362 parser.add_option('-m', dest='message',
4363 help="override review description")
4364 parser.add_option('-f', action='store_true', dest='force',
4365 help="force yes to questions (don't prompt)")
4366 parser.add_option('-c', dest='contributor',
4367 help="external contributor for patch (appended to " +
4368 "description and used as author for git). Should be " +
4369 "formatted as 'First Last <email@example.com>'")
4370 add_git_similarity(parser)
4371 auth.add_auth_options(parser)
4372 (options, args) = parser.parse_args(args)
4373 auth_config = auth.extract_auth_config_from_options(options)
4374
4375 cl = Changelist(auth_config=auth_config)
4376
4377 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4378 if cl.IsGerrit():
4379 if options.message:
4380 # This could be implemented, but it requires sending a new patch to
4381 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4382 # Besides, Gerrit has the ability to change the commit message on submit
4383 # automatically, thus there is no need to support this option (so far?).
4384 parser.error('-m MESSAGE option is not supported for Gerrit.')
4385 if options.contributor:
4386 parser.error(
4387 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4388 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4389 'the contributor\'s "name <email>". If you can\'t upload such a '
4390 'commit for review, contact your repository admin and request'
4391 '"Forge-Author" permission.')
4392 if not cl.GetIssue():
4393 DieWithError('You must upload the change first to Gerrit.\n'
4394 ' If you would rather have `git cl land` upload '
4395 'automatically for you, see http://crbug.com/642759')
4396 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4397 options.verbose)
4398
4399 current = cl.GetBranch()
4400 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4401 if remote == '.':
4402 print()
4403 print('Attempting to push branch %r into another local branch!' % current)
4404 print()
4405 print('Either reparent this branch on top of origin/master:')
4406 print(' git reparent-branch --root')
4407 print()
4408 print('OR run `git rebase-update` if you think the parent branch is ')
4409 print('already committed.')
4410 print()
4411 print(' Current parent: %r' % upstream_branch)
4412 return 1
4413
4414 if not args:
4415 # Default to merging against our best guess of the upstream branch.
4416 args = [cl.GetUpstreamBranch()]
4417
4418 if options.contributor:
4419 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
4420 print("Please provide contibutor as 'First Last <email@example.com>'")
4421 return 1
4422
4423 base_branch = args[0]
4424
4425 if git_common.is_dirty_git_tree('land'):
4426 return 1
4427
4428 # This rev-list syntax means "show all commits not in my branch that
4429 # are in base_branch".
4430 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4431 base_branch]).splitlines()
4432 if upstream_commits:
4433 print('Base branch "%s" has %d commits '
4434 'not in this branch.' % (base_branch, len(upstream_commits)))
4435 print('Run "git merge %s" before attempting to land.' % base_branch)
4436 return 1
4437
4438 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
4439 if not options.bypass_hooks:
4440 author = None
4441 if options.contributor:
4442 author = re.search(r'\<(.*)\>', options.contributor).group(1)
4443 hook_results = cl.RunHook(
4444 committing=True,
4445 may_prompt=not options.force,
4446 verbose=options.verbose,
4447 change=cl.GetChange(merge_base, author))
4448 if not hook_results.should_continue():
4449 return 1
4450
4451 # Check the tree status if the tree status URL is set.
4452 status = GetTreeStatus()
4453 if 'closed' == status:
4454 print('The tree is closed. Please wait for it to reopen. Use '
4455 '"git cl land --bypass-hooks" to commit on a closed tree.')
4456 return 1
4457 elif 'unknown' == status:
4458 print('Unable to determine tree status. Please verify manually and '
4459 'use "git cl land --bypass-hooks" to commit on a closed tree.')
4460 return 1
4461
4462 change_desc = ChangeDescription(options.message)
4463 if not change_desc.description and cl.GetIssue():
4464 change_desc = ChangeDescription(cl.GetDescription())
4465
4466 if not change_desc.description:
4467 if not cl.GetIssue() and options.bypass_hooks:
4468 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
4469 else:
4470 print('No description set.')
4471 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
4472 return 1
4473
4474 # Keep a separate copy for the commit message, because the commit message
4475 # contains the link to the Rietveld issue, while the Rietveld message contains
4476 # the commit viewvc url.
4477 if cl.GetIssue():
4478 change_desc.update_reviewers(cl.GetApprovingReviewers())
4479
4480 commit_desc = ChangeDescription(change_desc.description)
4481 if cl.GetIssue():
4482 # Xcode won't linkify this URL unless there is a non-whitespace character
4483 # after it. Add a period on a new line to circumvent this. Also add a space
4484 # before the period to make sure that Gitiles continues to correctly resolve
4485 # the URL.
4486 commit_desc.append_footer('Review-Url: %s .' % cl.GetIssueURL())
4487 if options.contributor:
4488 commit_desc.append_footer('Patch from %s.' % options.contributor)
4489
4490 print('Description:')
4491 print(commit_desc.description)
4492
4493 branches = [merge_base, cl.GetBranchRef()]
4494 if not options.force:
4495 print_stats(options.similarity, options.find_copies, branches)
4496
4497 # We want to squash all this branch's commits into one commit with the proper
4498 # description. We do this by doing a "reset --soft" to the base branch (which
4499 # keeps the working copy the same), then landing that.
4500 MERGE_BRANCH = 'git-cl-commit'
4501 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4502 # Delete the branches if they exist.
4503 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4504 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4505 result = RunGitWithCode(showref_cmd)
4506 if result[0] == 0:
4507 RunGit(['branch', '-D', branch])
4508
4509 # We might be in a directory that's present in this branch but not in the
4510 # trunk. Move up to the top of the tree so that git commands that expect a
4511 # valid CWD won't fail after we check out the merge branch.
4512 rel_base_path = settings.GetRelativeRoot()
4513 if rel_base_path:
4514 os.chdir(rel_base_path)
4515
4516 # Stuff our change into the merge branch.
4517 # We wrap in a try...finally block so if anything goes wrong,
4518 # we clean up the branches.
4519 retcode = -1
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004520 revision = None
4521 try:
4522 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
4523 RunGit(['reset', '--soft', merge_base])
4524 if options.contributor:
4525 RunGit(
4526 [
4527 'commit', '--author', options.contributor,
4528 '-m', commit_desc.description,
4529 ])
4530 else:
4531 RunGit(['commit', '-m', commit_desc.description])
4532
4533 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
4534 mirror = settings.GetGitMirror(remote)
4535 if mirror:
4536 pushurl = mirror.url
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004537 git_numberer_enabled = _is_git_numberer_enabled(pushurl, branch)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004538 else:
4539 pushurl = remote # Usually, this is 'origin'.
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004540 git_numberer_enabled = _is_git_numberer_enabled(
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004541 RunGit(['config', 'remote.%s.url' % remote]).strip(), branch)
4542
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004543 if git_numberer_enabled:
4544 # TODO(tandrii): maybe do autorebase + retry on failure
4545 # http://crbug.com/682934, but better just use Gerrit :)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004546 logging.debug('Adding git number footers')
4547 parent_msg = RunGit(['show', '-s', '--format=%B', merge_base]).strip()
4548 commit_desc.update_with_git_number_footers(merge_base, parent_msg,
4549 branch)
4550 # Ensure timestamps are monotonically increasing.
4551 timestamp = max(1 + _get_committer_timestamp(merge_base),
4552 _get_committer_timestamp('HEAD'))
4553 _git_amend_head(commit_desc.description, timestamp)
4554 change_desc = ChangeDescription(commit_desc.description)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004555
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004556 retcode, output = RunGitWithCode(
4557 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004558 if retcode == 0:
4559 revision = RunGit(['rev-parse', 'HEAD']).strip()
4560 logging.debug(output)
4561 except: # pylint: disable=bare-except
4562 if _IS_BEING_TESTED:
4563 logging.exception('this is likely your ACTUAL cause of test failure.\n'
4564 + '-' * 30 + '8<' + '-' * 30)
4565 logging.error('\n' + '-' * 30 + '8<' + '-' * 30 + '\n\n\n')
4566 raise
4567 finally:
4568 # And then swap back to the original branch and clean up.
4569 RunGit(['checkout', '-q', cl.GetBranch()])
4570 RunGit(['branch', '-D', MERGE_BRANCH])
4571
4572 if not revision:
4573 print('Failed to push. If this persists, please file a bug.')
4574 return 1
4575
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004576 if cl.GetIssue():
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004577 viewvc_url = settings.GetViewVCUrl()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004578 if viewvc_url and revision:
4579 change_desc.append_footer(
4580 'Committed: %s%s' % (viewvc_url, revision))
4581 elif revision:
4582 change_desc.append_footer('Committed: %s' % (revision,))
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004583 print('Closing issue '
4584 '(you may be prompted for your codereview password)...')
4585 cl.UpdateDescription(change_desc.description)
4586 cl.CloseIssue()
4587 props = cl.GetIssueProperties()
4588 patch_num = len(props['patchsets'])
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004589 comment = "Committed patchset #%d (id:%d) manually as %s" % (
4590 patch_num, props['patchsets'][-1], revision)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004591 if options.bypass_hooks:
4592 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4593 else:
4594 comment += ' (presubmit successful).'
4595 cl.RpcServer().add_comment(cl.GetIssue(), comment)
4596
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004597 if os.path.isfile(POSTUPSTREAM_HOOK):
4598 RunCommand([POSTUPSTREAM_HOOK, merge_base], error_ok=True)
4599
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004600 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004601
4602
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004603@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004604def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004605 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004606 parser.add_option('-b', dest='newbranch',
4607 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004608 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004609 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004610 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4611 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004612 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004613 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004614 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004615 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004616 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004617 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004618
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004619
4620 group = optparse.OptionGroup(
4621 parser,
4622 'Options for continuing work on the current issue uploaded from a '
4623 'different clone (e.g. different machine). Must be used independently '
4624 'from the other options. No issue number should be specified, and the '
4625 'branch must have an issue number associated with it')
4626 group.add_option('--reapply', action='store_true', dest='reapply',
4627 help='Reset the branch and reapply the issue.\n'
4628 'CAUTION: This will undo any local changes in this '
4629 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004630
4631 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004632 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004633 parser.add_option_group(group)
4634
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004635 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004636 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004637 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004638 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004639 auth_config = auth.extract_auth_config_from_options(options)
4640
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004641
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004642 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004643 if options.newbranch:
4644 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004645 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004646 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004647
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004648 cl = Changelist(auth_config=auth_config,
4649 codereview=options.forced_codereview)
4650 if not cl.GetIssue():
4651 parser.error('current branch must have an associated issue')
4652
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004653 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004654 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004655 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004656
4657 RunGit(['reset', '--hard', upstream])
4658 if options.pull:
4659 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004660
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004661 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4662 options.directory)
4663
4664 if len(args) != 1 or not args[0]:
4665 parser.error('Must specify issue number or url')
4666
4667 # We don't want uncommitted changes mixed up with the patch.
4668 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004669 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004670
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004671 if options.newbranch:
4672 if options.force:
4673 RunGit(['branch', '-D', options.newbranch],
4674 stderr=subprocess2.PIPE, error_ok=True)
4675 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004676 elif not GetCurrentBranch():
4677 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004678
4679 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4680
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004681 if cl.IsGerrit():
4682 if options.reject:
4683 parser.error('--reject is not supported with Gerrit codereview.')
4684 if options.nocommit:
4685 parser.error('--nocommit is not supported with Gerrit codereview.')
4686 if options.directory:
4687 parser.error('--directory is not supported with Gerrit codereview.')
4688
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004689 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004690 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004691
4692
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004693def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004694 """Fetches the tree status and returns either 'open', 'closed',
4695 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004696 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004697 if url:
4698 status = urllib2.urlopen(url).read().lower()
4699 if status.find('closed') != -1 or status == '0':
4700 return 'closed'
4701 elif status.find('open') != -1 or status == '1':
4702 return 'open'
4703 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004704 return 'unset'
4705
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004706
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004707def GetTreeStatusReason():
4708 """Fetches the tree status from a json url and returns the message
4709 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004710 url = settings.GetTreeStatusUrl()
4711 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004712 connection = urllib2.urlopen(json_url)
4713 status = json.loads(connection.read())
4714 connection.close()
4715 return status['message']
4716
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004717
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004718def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004719 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004720 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004721 status = GetTreeStatus()
4722 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004723 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004724 return 2
4725
vapiera7fbd5a2016-06-16 09:17:49 -07004726 print('The tree is %s' % status)
4727 print()
4728 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004729 if status != 'open':
4730 return 1
4731 return 0
4732
4733
maruel@chromium.org15192402012-09-06 12:38:29 +00004734def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004735 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004736 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004737 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004738 '-b', '--bot', action='append',
4739 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4740 'times to specify multiple builders. ex: '
4741 '"-b win_rel -b win_layout". See '
4742 'the try server waterfall for the builders name and the tests '
4743 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004744 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004745 '-B', '--bucket', default='',
4746 help=('Buildbucket bucket to send the try requests.'))
4747 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004748 '-m', '--master', default='',
4749 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004750 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004751 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004752 help='Revision to use for the try job; default: the revision will '
4753 'be determined by the try recipe that builder runs, which usually '
4754 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004755 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004756 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004757 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004758 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004759 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004760 '--project',
4761 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004762 'in recipe to determine to which repository or directory to '
4763 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004764 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004765 '-p', '--property', dest='properties', action='append', default=[],
4766 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004767 'key2=value2 etc. The value will be treated as '
4768 'json if decodable, or as string otherwise. '
4769 'NOTE: using this may make your try job not usable for CQ, '
4770 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004771 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004772 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4773 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004774 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004775 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004776 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004777 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004778
machenbach@chromium.org45453142015-09-15 08:45:22 +00004779 # Make sure that all properties are prop=value pairs.
4780 bad_params = [x for x in options.properties if '=' not in x]
4781 if bad_params:
4782 parser.error('Got properties with missing "=": %s' % bad_params)
4783
maruel@chromium.org15192402012-09-06 12:38:29 +00004784 if args:
4785 parser.error('Unknown arguments: %s' % args)
4786
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004787 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004788 if not cl.GetIssue():
4789 parser.error('Need to upload first')
4790
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01004791 if cl.IsGerrit():
4792 # HACK: warm up Gerrit change detail cache to save on RPCs.
4793 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
4794
tandriie113dfd2016-10-11 10:20:12 -07004795 error_message = cl.CannotTriggerTryJobReason()
4796 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004797 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004798
borenet6c0efe62016-10-19 08:13:29 -07004799 if options.bucket and options.master:
4800 parser.error('Only one of --bucket and --master may be used.')
4801
qyearsley1fdfcb62016-10-24 13:22:03 -07004802 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004803
qyearsleydd49f942016-10-28 11:57:22 -07004804 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4805 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004806 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004807 if options.verbose:
4808 print('git cl try with no bots now defaults to CQ Dry Run.')
4809 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004810
borenet6c0efe62016-10-19 08:13:29 -07004811 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004812 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004813 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004814 'of bot requires an initial job from a parent (usually a builder). '
4815 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004816 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004817 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004818
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004819 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004820 # TODO(tandrii): Checking local patchset against remote patchset is only
4821 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4822 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07004823 print('Warning: Codereview server has newer patchsets (%s) than most '
4824 'recent upload from local checkout (%s). Did a previous upload '
4825 'fail?\n'
4826 'By default, git cl try uses the latest patchset from '
4827 'codereview, continuing to use patchset %s.\n' %
4828 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07004829
tandrii568043b2016-10-11 07:49:18 -07004830 try:
borenet6c0efe62016-10-19 08:13:29 -07004831 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
4832 patchset)
tandrii568043b2016-10-11 07:49:18 -07004833 except BuildbucketResponseException as ex:
4834 print('ERROR: %s' % ex)
4835 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004836 return 0
4837
4838
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004839def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004840 """Prints info about try jobs associated with current CL."""
4841 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004842 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004843 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004844 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004845 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004846 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004847 '--color', action='store_true', default=setup_color.IS_TTY,
4848 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004849 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004850 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4851 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004852 group.add_option(
4853 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004854 parser.add_option_group(group)
4855 auth.add_auth_options(parser)
4856 options, args = parser.parse_args(args)
4857 if args:
4858 parser.error('Unrecognized args: %s' % ' '.join(args))
4859
4860 auth_config = auth.extract_auth_config_from_options(options)
4861 cl = Changelist(auth_config=auth_config)
4862 if not cl.GetIssue():
4863 parser.error('Need to upload first')
4864
tandrii221ab252016-10-06 08:12:04 -07004865 patchset = options.patchset
4866 if not patchset:
4867 patchset = cl.GetMostRecentPatchset()
4868 if not patchset:
4869 parser.error('Codereview doesn\'t know about issue %s. '
4870 'No access to issue or wrong issue number?\n'
4871 'Either upload first, or pass --patchset explicitely' %
4872 cl.GetIssue())
4873
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004874 # TODO(tandrii): Checking local patchset against remote patchset is only
4875 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4876 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07004877 print('Warning: Codereview server has newer patchsets (%s) than most '
4878 'recent upload from local checkout (%s). Did a previous upload '
4879 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07004880 'By default, git cl try-results uses the latest patchset from '
4881 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07004882 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004883 try:
tandrii221ab252016-10-06 08:12:04 -07004884 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004885 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004886 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004887 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004888 if options.json:
4889 write_try_results_json(options.json, jobs)
4890 else:
4891 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004892 return 0
4893
4894
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004895@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004896def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004897 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004898 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004899 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004900 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004901
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004902 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004903 if args:
4904 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004905 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004906 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004907 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004908 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004909
4910 # Clear configured merge-base, if there is one.
4911 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004912 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004913 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004914 return 0
4915
4916
thestig@chromium.org00858c82013-12-02 23:08:03 +00004917def CMDweb(parser, args):
4918 """Opens the current CL in the web browser."""
4919 _, args = parser.parse_args(args)
4920 if args:
4921 parser.error('Unrecognized args: %s' % ' '.join(args))
4922
4923 issue_url = Changelist().GetIssueURL()
4924 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004925 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004926 return 1
4927
4928 webbrowser.open(issue_url)
4929 return 0
4930
4931
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004932def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004933 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004934 parser.add_option('-d', '--dry-run', action='store_true',
4935 help='trigger in dry run mode')
4936 parser.add_option('-c', '--clear', action='store_true',
4937 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004938 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004939 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004940 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004941 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004942 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004943 if args:
4944 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004945 if options.dry_run and options.clear:
4946 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4947
iannuccie53c9352016-08-17 14:40:40 -07004948 cl = Changelist(auth_config=auth_config, issue=options.issue,
4949 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004950 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004951 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004952 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07004953 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004954 state = _CQState.DRY_RUN
4955 else:
4956 state = _CQState.COMMIT
4957 if not cl.GetIssue():
4958 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07004959 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004960 return 0
4961
4962
groby@chromium.org411034a2013-02-26 15:12:01 +00004963def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004964 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07004965 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004966 auth.add_auth_options(parser)
4967 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004968 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004969 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004970 if args:
4971 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07004972 cl = Changelist(auth_config=auth_config, issue=options.issue,
4973 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00004974 # Ensure there actually is an issue to close.
4975 cl.GetDescription()
4976 cl.CloseIssue()
4977 return 0
4978
4979
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004980def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004981 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07004982 parser.add_option(
4983 '--stat',
4984 action='store_true',
4985 dest='stat',
4986 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004987 auth.add_auth_options(parser)
4988 options, args = parser.parse_args(args)
4989 auth_config = auth.extract_auth_config_from_options(options)
4990 if args:
4991 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004992
4993 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004994 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004995 # Staged changes would be committed along with the patch from last
4996 # upload, hence counted toward the "last upload" side in the final
4997 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004998 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004999 return 1
5000
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005001 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005002 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005003 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005004 if not issue:
5005 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005006 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005007 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005008
5009 # Create a new branch based on the merge-base
5010 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005011 # Clear cached branch in cl object, to avoid overwriting original CL branch
5012 # properties.
5013 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005014 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005015 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005016 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005017 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005018 return rtn
5019
wychen@chromium.org06928532015-02-03 02:11:29 +00005020 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005021 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005022 cmd = ['git', 'diff']
5023 if options.stat:
5024 cmd.append('--stat')
5025 cmd.extend([TMP_BRANCH, branch, '--'])
5026 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005027 finally:
5028 RunGit(['checkout', '-q', branch])
5029 RunGit(['branch', '-D', TMP_BRANCH])
5030
5031 return 0
5032
5033
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005034def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005035 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005036 parser.add_option(
5037 '--no-color',
5038 action='store_true',
5039 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005040 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005041 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005042 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005043
5044 author = RunGit(['config', 'user.email']).strip() or None
5045
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005046 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005047
5048 if args:
5049 if len(args) > 1:
5050 parser.error('Unknown args')
5051 base_branch = args[0]
5052 else:
5053 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005054 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005055
5056 change = cl.GetChange(base_branch, None)
5057 return owners_finder.OwnersFinder(
5058 [f.LocalPath() for f in
5059 cl.GetChange(base_branch, None).AffectedFiles()],
5060 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005061 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005062 disable_color=options.no_color).run()
5063
5064
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005065def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005066 """Generates a diff command."""
5067 # Generate diff for the current branch's changes.
5068 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005069 upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005070
5071 if args:
5072 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005073 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005074 diff_cmd.append(arg)
5075 else:
5076 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005077
5078 return diff_cmd
5079
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005080
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005081def MatchingFileType(file_name, extensions):
5082 """Returns true if the file name ends with one of the given extensions."""
5083 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005084
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005085
enne@chromium.org555cfe42014-01-29 18:21:39 +00005086@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005087def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005088 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005089 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005090 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005091 parser.add_option('--full', action='store_true',
5092 help='Reformat the full content of all touched files')
5093 parser.add_option('--dry-run', action='store_true',
5094 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005095 parser.add_option('--python', action='store_true',
5096 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005097 parser.add_option('--js', action='store_true',
5098 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005099 parser.add_option('--diff', action='store_true',
5100 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005101 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005102
Daniel Chengc55eecf2016-12-30 03:11:02 -08005103 # Normalize any remaining args against the current path, so paths relative to
5104 # the current directory are still resolved as expected.
5105 args = [os.path.join(os.getcwd(), arg) for arg in args]
5106
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005107 # git diff generates paths against the root of the repository. Change
5108 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005109 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005110 if rel_base_path:
5111 os.chdir(rel_base_path)
5112
digit@chromium.org29e47272013-05-17 17:01:46 +00005113 # Grab the merge-base commit, i.e. the upstream commit of the current
5114 # branch when it was created or the last time it was rebased. This is
5115 # to cover the case where the user may have called "git fetch origin",
5116 # moving the origin branch to a newer commit, but hasn't rebased yet.
5117 upstream_commit = None
5118 cl = Changelist()
5119 upstream_branch = cl.GetUpstreamBranch()
5120 if upstream_branch:
5121 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5122 upstream_commit = upstream_commit.strip()
5123
5124 if not upstream_commit:
5125 DieWithError('Could not find base commit for this branch. '
5126 'Are you in detached state?')
5127
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005128 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5129 diff_output = RunGit(changed_files_cmd)
5130 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005131 # Filter out files deleted by this CL
5132 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005133
Christopher Lamc5ba6922017-01-24 11:19:14 +11005134 if opts.js:
5135 CLANG_EXTS.append('.js')
5136
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005137 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5138 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5139 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005140 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005141
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005142 top_dir = os.path.normpath(
5143 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5144
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005145 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5146 # formatted. This is used to block during the presubmit.
5147 return_value = 0
5148
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005149 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005150 # Locate the clang-format binary in the checkout
5151 try:
5152 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005153 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005154 DieWithError(e)
5155
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005156 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005157 cmd = [clang_format_tool]
5158 if not opts.dry_run and not opts.diff:
5159 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005160 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005161 if opts.diff:
5162 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005163 else:
5164 env = os.environ.copy()
5165 env['PATH'] = str(os.path.dirname(clang_format_tool))
5166 try:
5167 script = clang_format.FindClangFormatScriptInChromiumTree(
5168 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005169 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005170 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005171
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005172 cmd = [sys.executable, script, '-p0']
5173 if not opts.dry_run and not opts.diff:
5174 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005175
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005176 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5177 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005178
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005179 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5180 if opts.diff:
5181 sys.stdout.write(stdout)
5182 if opts.dry_run and len(stdout) > 0:
5183 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005184
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005185 # Similar code to above, but using yapf on .py files rather than clang-format
5186 # on C/C++ files
5187 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005188 yapf_tool = gclient_utils.FindExecutable('yapf')
5189 if yapf_tool is None:
5190 DieWithError('yapf not found in PATH')
5191
5192 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005193 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005194 cmd = [yapf_tool]
5195 if not opts.dry_run and not opts.diff:
5196 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005197 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005198 if opts.diff:
5199 sys.stdout.write(stdout)
5200 else:
5201 # TODO(sbc): yapf --lines mode still has some issues.
5202 # https://github.com/google/yapf/issues/154
5203 DieWithError('--python currently only works with --full')
5204
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005205 # Dart's formatter does not have the nice property of only operating on
5206 # modified chunks, so hard code full.
5207 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005208 try:
5209 command = [dart_format.FindDartFmtToolInChromiumTree()]
5210 if not opts.dry_run and not opts.diff:
5211 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005212 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005213
ppi@chromium.org6593d932016-03-03 15:41:15 +00005214 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005215 if opts.dry_run and stdout:
5216 return_value = 2
5217 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005218 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5219 'found in this checkout. Files in other languages are still '
5220 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005221
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005222 # Format GN build files. Always run on full build files for canonical form.
5223 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005224 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005225 if opts.dry_run or opts.diff:
5226 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005227 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005228 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5229 shell=sys.platform == 'win32',
5230 cwd=top_dir)
5231 if opts.dry_run and gn_ret == 2:
5232 return_value = 2 # Not formatted.
5233 elif opts.diff and gn_ret == 2:
5234 # TODO this should compute and print the actual diff.
5235 print("This change has GN build file diff for " + gn_diff_file)
5236 elif gn_ret != 0:
5237 # For non-dry run cases (and non-2 return values for dry-run), a
5238 # nonzero error code indicates a failure, probably because the file
5239 # doesn't parse.
5240 DieWithError("gn format failed on " + gn_diff_file +
5241 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005242
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005243 metrics_xml_files = [
5244 'tools/metrics/actions/actions.xml',
5245 'tools/metrics/histograms/histograms.xml',
5246 'tools/metrics/rappor/rappor.xml']
5247 for xml_file in metrics_xml_files:
5248 if xml_file in diff_files:
5249 tool_dir = top_dir + '/' + os.path.dirname(xml_file)
5250 cmd = [tool_dir + '/pretty_print.py', '--non-interactive']
5251 if opts.dry_run or opts.diff:
5252 cmd.append('--diff')
5253 stdout = RunCommand(cmd, cwd=top_dir)
5254 if opts.diff:
5255 sys.stdout.write(stdout)
5256 if opts.dry_run and stdout:
5257 return_value = 2 # Not formatted.
5258
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005259 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005260
5261
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005262@subcommand.usage('<codereview url or issue id>')
5263def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005264 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005265 _, args = parser.parse_args(args)
5266
5267 if len(args) != 1:
5268 parser.print_help()
5269 return 1
5270
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005271 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005272 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005273 parser.print_help()
5274 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005275 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005276
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005277 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005278 output = RunGit(['config', '--local', '--get-regexp',
5279 r'branch\..*\.%s' % issueprefix],
5280 error_ok=True)
5281 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005282 if issue == target_issue:
5283 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005284
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005285 branches = []
5286 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005287 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005288 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005289 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005290 return 1
5291 if len(branches) == 1:
5292 RunGit(['checkout', branches[0]])
5293 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005294 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005295 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005296 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005297 which = raw_input('Choose by index: ')
5298 try:
5299 RunGit(['checkout', branches[int(which)]])
5300 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005301 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005302 return 1
5303
5304 return 0
5305
5306
maruel@chromium.org29404b52014-09-08 22:58:00 +00005307def CMDlol(parser, args):
5308 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005309 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005310 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5311 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5312 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005313 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005314 return 0
5315
5316
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005317class OptionParser(optparse.OptionParser):
5318 """Creates the option parse and add --verbose support."""
5319 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005320 optparse.OptionParser.__init__(
5321 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005322 self.add_option(
5323 '-v', '--verbose', action='count', default=0,
5324 help='Use 2 times for more debugging info')
5325
5326 def parse_args(self, args=None, values=None):
5327 options, args = optparse.OptionParser.parse_args(self, args, values)
5328 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005329 logging.basicConfig(
5330 level=levels[min(options.verbose, len(levels) - 1)],
5331 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5332 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005333 return options, args
5334
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005335
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005336def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005337 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005338 print('\nYour python version %s is unsupported, please upgrade.\n' %
5339 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005340 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005341
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005342 # Reload settings.
5343 global settings
5344 settings = Settings()
5345
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005346 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005347 dispatcher = subcommand.CommandDispatcher(__name__)
5348 try:
5349 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005350 except auth.AuthenticationError as e:
5351 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005352 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005353 if e.code != 500:
5354 raise
5355 DieWithError(
5356 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5357 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005358 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005359
5360
5361if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005362 # These affect sys.stdout so do it outside of main() to simplify mocks in
5363 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005364 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005365 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005366 try:
5367 sys.exit(main(sys.argv[1:]))
5368 except KeyboardInterrupt:
5369 sys.stderr.write('interrupted\n')
5370 sys.exit(1)