blob: 797cbc6722a5f6426337a89e4f07fcfb03acb65f [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
Andrii Shyshkalov353637c2017-03-14 16:52:18 +010025import shutil
ukai@chromium.org78c4b982012-02-14 02:20:26 +000026import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000027import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028import textwrap
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000029import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000030import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000031import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000032import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000033import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000034import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000035
36try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080037 import readline # pylint: disable=import-error,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000038except ImportError:
39 pass
40
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000041from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000042from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000043from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000044import auth
skobes6468b902016-10-24 08:45:10 -070045import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000046import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000047import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000048import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000049import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000050import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000051import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000052import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000053import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000054import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000055import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000056import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000057import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000058import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000059import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000060import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000061import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062import watchlists
63
tandrii7400cf02016-06-21 08:48:07 -070064__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000065
tandrii9d2c7a32016-06-22 03:42:45 -070066COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070067DEFAULT_SERVER = 'https://codereview.chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080068POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000069DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +000070REFS_THAT_ALIAS_TO_OTHER_REFS = {
71 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
72 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
73}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074
thestig@chromium.org44202a22014-03-11 19:22:18 +000075# Valid extensions for files we want to lint.
76DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
77DEFAULT_LINT_IGNORE_REGEX = r"$^"
78
borenet6c0efe62016-10-19 08:13:29 -070079# Buildbucket master name prefix.
80MASTER_PREFIX = 'master.'
81
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000082# Shortcut since it quickly becomes redundant.
83Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000084
maruel@chromium.orgddd59412011-11-30 14:20:38 +000085# Initialized in main()
86settings = None
87
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010088# Used by tests/git_cl_test.py to add extra logging.
89# Inside the weirdly failing test, add this:
90# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
91# And scroll up to see the strack trace printed.
92_IS_BEING_TESTED = False
93
maruel@chromium.orgddd59412011-11-30 14:20:38 +000094
Christopher Lamf732cd52017-01-24 12:40:11 +110095def DieWithError(message, change_desc=None):
96 if change_desc:
97 SaveDescriptionBackup(change_desc)
98
vapiera7fbd5a2016-06-16 09:17:49 -070099 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000100 sys.exit(1)
101
102
Christopher Lamf732cd52017-01-24 12:40:11 +1100103def SaveDescriptionBackup(change_desc):
104 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
105 print('\nError after CL description prompt -- saving description to %s\n' %
106 backup_path)
107 backup_file = open(backup_path, 'w')
108 backup_file.write(change_desc.description)
109 backup_file.close()
110
111
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000112def GetNoGitPagerEnv():
113 env = os.environ.copy()
114 # 'cat' is a magical git string that disables pagers on all platforms.
115 env['GIT_PAGER'] = 'cat'
116 return env
117
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000118
bsep@chromium.org627d9002016-04-29 00:00:52 +0000119def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000120 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000121 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000122 except subprocess2.CalledProcessError as e:
123 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000124 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000125 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000126 'Command "%s" failed.\n%s' % (
127 ' '.join(args), error_message or e.stdout or ''))
128 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000129
130
131def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000132 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000133 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000134
135
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000136def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000137 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700138 if suppress_stderr:
139 stderr = subprocess2.VOID
140 else:
141 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000142 try:
tandrii5d48c322016-08-18 16:19:37 -0700143 (out, _), code = subprocess2.communicate(['git'] + args,
144 env=GetNoGitPagerEnv(),
145 stdout=subprocess2.PIPE,
146 stderr=stderr)
147 return code, out
148 except subprocess2.CalledProcessError as e:
149 logging.debug('Failed running %s', args)
150 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000151
152
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000153def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000154 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000155 return RunGitWithCode(args, suppress_stderr=True)[1]
156
157
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000158def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000159 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000160 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000161 return (version.startswith(prefix) and
162 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000163
164
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000165def BranchExists(branch):
166 """Return True if specified branch exists."""
167 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
168 suppress_stderr=True)
169 return not code
170
171
tandrii2a16b952016-10-19 07:09:44 -0700172def time_sleep(seconds):
173 # Use this so that it can be mocked in tests without interfering with python
174 # system machinery.
175 import time # Local import to discourage others from importing time globally.
176 return time.sleep(seconds)
177
178
maruel@chromium.org90541732011-04-01 17:54:18 +0000179def ask_for_data(prompt):
180 try:
181 return raw_input(prompt)
182 except KeyboardInterrupt:
183 # Hide the exception.
184 sys.exit(1)
185
186
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100187def confirm_or_exit(prefix='', action='confirm'):
188 """Asks user to press enter to continue or press Ctrl+C to abort."""
189 if not prefix or prefix.endswith('\n'):
190 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100191 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100192 mid = ' Press'
193 elif prefix.endswith(' '):
194 mid = 'press'
195 else:
196 mid = ' press'
197 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
198
199
200def ask_for_explicit_yes(prompt):
201 """Returns whether user typed 'y' or 'yes' to confirm the given prompt"""
202 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
203 while True:
204 if 'yes'.startswith(result):
205 return True
206 if 'no'.startswith(result):
207 return False
208 result = ask_for_data('Please, type yes or no: ').lower()
209
210
tandrii5d48c322016-08-18 16:19:37 -0700211def _git_branch_config_key(branch, key):
212 """Helper method to return Git config key for a branch."""
213 assert branch, 'branch name is required to set git config for it'
214 return 'branch.%s.%s' % (branch, key)
215
216
217def _git_get_branch_config_value(key, default=None, value_type=str,
218 branch=False):
219 """Returns git config value of given or current branch if any.
220
221 Returns default in all other cases.
222 """
223 assert value_type in (int, str, bool)
224 if branch is False: # Distinguishing default arg value from None.
225 branch = GetCurrentBranch()
226
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000227 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700228 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000229
tandrii5d48c322016-08-18 16:19:37 -0700230 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700231 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700232 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700233 # git config also has --int, but apparently git config suffers from integer
234 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700235 args.append(_git_branch_config_key(branch, key))
236 code, out = RunGitWithCode(args)
237 if code == 0:
238 value = out.strip()
239 if value_type == int:
240 return int(value)
241 if value_type == bool:
242 return bool(value.lower() == 'true')
243 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000244 return default
245
246
tandrii5d48c322016-08-18 16:19:37 -0700247def _git_set_branch_config_value(key, value, branch=None, **kwargs):
248 """Sets the value or unsets if it's None of a git branch config.
249
250 Valid, though not necessarily existing, branch must be provided,
251 otherwise currently checked out branch is used.
252 """
253 if not branch:
254 branch = GetCurrentBranch()
255 assert branch, 'a branch name OR currently checked out branch is required'
256 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700257 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700258 if value is None:
259 args.append('--unset')
260 elif isinstance(value, bool):
261 args.append('--bool')
262 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700263 else:
tandrii33a46ff2016-08-23 05:53:40 -0700264 # git config also has --int, but apparently git config suffers from integer
265 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700266 value = str(value)
267 args.append(_git_branch_config_key(branch, key))
268 if value is not None:
269 args.append(value)
270 RunGit(args, **kwargs)
271
272
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100273def _get_committer_timestamp(commit):
274 """Returns unix timestamp as integer of a committer in a commit.
275
276 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
277 """
278 # Git also stores timezone offset, but it only affects visual display,
279 # actual point in time is defined by this timestamp only.
280 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
281
282
283def _git_amend_head(message, committer_timestamp):
284 """Amends commit with new message and desired committer_timestamp.
285
286 Sets committer timezone to UTC.
287 """
288 env = os.environ.copy()
289 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
290 return RunGit(['commit', '--amend', '-m', message], env=env)
291
292
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000293def add_git_similarity(parser):
294 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700295 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000296 help='Sets the percentage that a pair of files need to match in order to'
297 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000298 parser.add_option(
299 '--find-copies', action='store_true',
300 help='Allows git to look for copies.')
301 parser.add_option(
302 '--no-find-copies', action='store_false', dest='find_copies',
303 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000304
305 old_parser_args = parser.parse_args
306 def Parse(args):
307 options, args = old_parser_args(args)
308
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000309 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700310 options.similarity = _git_get_branch_config_value(
311 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000312 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000313 print('Note: Saving similarity of %d%% in git config.'
314 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700315 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000316
iannucci@chromium.org79540052012-10-19 23:15:26 +0000317 options.similarity = max(0, min(options.similarity, 100))
318
319 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700320 options.find_copies = _git_get_branch_config_value(
321 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000322 else:
tandrii5d48c322016-08-18 16:19:37 -0700323 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000324
325 print('Using %d%% similarity for rename/copy detection. '
326 'Override with --similarity.' % options.similarity)
327
328 return options, args
329 parser.parse_args = Parse
330
331
machenbach@chromium.org45453142015-09-15 08:45:22 +0000332def _get_properties_from_options(options):
333 properties = dict(x.split('=', 1) for x in options.properties)
334 for key, val in properties.iteritems():
335 try:
336 properties[key] = json.loads(val)
337 except ValueError:
338 pass # If a value couldn't be evaluated, treat it as a string.
339 return properties
340
341
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000342def _prefix_master(master):
343 """Convert user-specified master name to full master name.
344
345 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
346 name, while the developers always use shortened master name
347 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
348 function does the conversion for buildbucket migration.
349 """
borenet6c0efe62016-10-19 08:13:29 -0700350 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000351 return master
borenet6c0efe62016-10-19 08:13:29 -0700352 return '%s%s' % (MASTER_PREFIX, master)
353
354
355def _unprefix_master(bucket):
356 """Convert bucket name to shortened master name.
357
358 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
359 name, while the developers always use shortened master name
360 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
361 function does the conversion for buildbucket migration.
362 """
363 if bucket.startswith(MASTER_PREFIX):
364 return bucket[len(MASTER_PREFIX):]
365 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000366
367
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000368def _buildbucket_retry(operation_name, http, *args, **kwargs):
369 """Retries requests to buildbucket service and returns parsed json content."""
370 try_count = 0
371 while True:
372 response, content = http.request(*args, **kwargs)
373 try:
374 content_json = json.loads(content)
375 except ValueError:
376 content_json = None
377
378 # Buildbucket could return an error even if status==200.
379 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000380 error = content_json.get('error')
381 if error.get('code') == 403:
382 raise BuildbucketResponseException(
383 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000384 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000385 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000386 raise BuildbucketResponseException(msg)
387
388 if response.status == 200:
389 if not content_json:
390 raise BuildbucketResponseException(
391 'Buildbucket returns invalid json content: %s.\n'
392 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
393 content)
394 return content_json
395 if response.status < 500 or try_count >= 2:
396 raise httplib2.HttpLib2Error(content)
397
398 # status >= 500 means transient failures.
399 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700400 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000401 try_count += 1
402 assert False, 'unreachable'
403
404
qyearsley1fdfcb62016-10-24 13:22:03 -0700405def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700406 """Returns a dict mapping bucket names to builders and tests,
407 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700408 """
qyearsleydd49f942016-10-28 11:57:22 -0700409 # If no bots are listed, we try to get a set of builders and tests based
410 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700411 if not options.bot:
412 change = changelist.GetChange(
413 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700414 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700415 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700416 change=change,
417 changed_files=change.LocalPaths(),
418 repository_root=settings.GetRoot(),
419 default_presubmit=None,
420 project=None,
421 verbose=options.verbose,
422 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700423 if masters is None:
424 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100425 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700426
qyearsley1fdfcb62016-10-24 13:22:03 -0700427 if options.bucket:
428 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700429 if options.master:
430 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700431
qyearsleydd49f942016-10-28 11:57:22 -0700432 # If bots are listed but no master or bucket, then we need to find out
433 # the corresponding master for each bot.
434 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
435 if error_message:
436 option_parser.error(
437 'Tryserver master cannot be found because: %s\n'
438 'Please manually specify the tryserver master, e.g. '
439 '"-m tryserver.chromium.linux".' % error_message)
440 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700441
442
qyearsley123a4682016-10-26 09:12:17 -0700443def _get_bucket_map_for_builders(builders):
444 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700445 map_url = 'https://builders-map.appspot.com/'
446 try:
qyearsley123a4682016-10-26 09:12:17 -0700447 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700448 except urllib2.URLError as e:
449 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
450 (map_url, e))
451 except ValueError as e:
452 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700453 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700454 return None, 'Failed to build master map.'
455
qyearsley123a4682016-10-26 09:12:17 -0700456 bucket_map = {}
457 for builder in builders:
qyearsley123a4682016-10-26 09:12:17 -0700458 masters = builders_map.get(builder, [])
459 if not masters:
qyearsley1fdfcb62016-10-24 13:22:03 -0700460 return None, ('No matching master for builder %s.' % builder)
qyearsley123a4682016-10-26 09:12:17 -0700461 if len(masters) > 1:
qyearsley1fdfcb62016-10-24 13:22:03 -0700462 return None, ('The builder name %s exists in multiple masters %s.' %
qyearsley123a4682016-10-26 09:12:17 -0700463 (builder, masters))
464 bucket = _prefix_master(masters[0])
465 bucket_map.setdefault(bucket, {})[builder] = []
466
467 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700468
469
borenet6c0efe62016-10-19 08:13:29 -0700470def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700471 category='git_cl_try', patchset=None):
qyearsley1fdfcb62016-10-24 13:22:03 -0700472 """Sends a request to Buildbucket to trigger try jobs for a changelist.
473
474 Args:
475 auth_config: AuthConfig for Rietveld.
476 changelist: Changelist that the try jobs are associated with.
477 buckets: A nested dict mapping bucket names to builders to tests.
478 options: Command-line options.
479 """
tandriide281ae2016-10-12 06:02:30 -0700480 assert changelist.GetIssue(), 'CL must be uploaded first'
481 codereview_url = changelist.GetCodereviewServer()
482 assert codereview_url, 'CL must be uploaded first'
483 patchset = patchset or changelist.GetMostRecentPatchset()
484 assert patchset, 'CL must be uploaded first'
485
486 codereview_host = urlparse.urlparse(codereview_url).hostname
487 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000488 http = authenticator.authorize(httplib2.Http())
489 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700490
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000491 buildbucket_put_url = (
492 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000493 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700494 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
495 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
496 hostname=codereview_host,
497 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000498 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700499
500 shared_parameters_properties = changelist.GetTryjobProperties(patchset)
501 shared_parameters_properties['category'] = category
502 if options.clobber:
503 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700504 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700505 if extra_properties:
506 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000507
508 batch_req_body = {'builds': []}
509 print_text = []
510 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700511 for bucket, builders_and_tests in sorted(buckets.iteritems()):
512 print_text.append('Bucket: %s' % bucket)
513 master = None
514 if bucket.startswith(MASTER_PREFIX):
515 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000516 for builder, tests in sorted(builders_and_tests.iteritems()):
517 print_text.append(' %s: %s' % (builder, tests))
518 parameters = {
519 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000520 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100521 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000522 'revision': options.revision,
523 }],
tandrii8c5a3532016-11-04 07:52:02 -0700524 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000525 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000526 if 'presubmit' in builder.lower():
527 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000528 if tests:
529 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700530
531 tags = [
532 'builder:%s' % builder,
533 'buildset:%s' % buildset,
534 'user_agent:git_cl_try',
535 ]
536 if master:
537 parameters['properties']['master'] = master
538 tags.append('master:%s' % master)
539
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000540 batch_req_body['builds'].append(
541 {
542 'bucket': bucket,
543 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000544 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700545 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000546 }
547 )
548
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000549 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700550 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000551 http,
552 buildbucket_put_url,
553 'PUT',
554 body=json.dumps(batch_req_body),
555 headers={'Content-Type': 'application/json'}
556 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000557 print_text.append('To see results here, run: git cl try-results')
558 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700559 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000560
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000561
tandrii221ab252016-10-06 08:12:04 -0700562def fetch_try_jobs(auth_config, changelist, buildbucket_host,
563 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700564 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000565
qyearsley53f48a12016-09-01 10:45:13 -0700566 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000567 """
tandrii221ab252016-10-06 08:12:04 -0700568 assert buildbucket_host
569 assert changelist.GetIssue(), 'CL must be uploaded first'
570 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
571 patchset = patchset or changelist.GetMostRecentPatchset()
572 assert patchset, 'CL must be uploaded first'
573
574 codereview_url = changelist.GetCodereviewServer()
575 codereview_host = urlparse.urlparse(codereview_url).hostname
576 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000577 if authenticator.has_cached_credentials():
578 http = authenticator.authorize(httplib2.Http())
579 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700580 print('Warning: Some results might be missing because %s' %
581 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700582 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000583 http = httplib2.Http()
584
585 http.force_exception_to_status_code = True
586
tandrii221ab252016-10-06 08:12:04 -0700587 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
588 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
589 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000590 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700591 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000592 params = {'tag': 'buildset:%s' % buildset}
593
594 builds = {}
595 while True:
596 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700597 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000598 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700599 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000600 for build in content.get('builds', []):
601 builds[build['id']] = build
602 if 'next_cursor' in content:
603 params['start_cursor'] = content['next_cursor']
604 else:
605 break
606 return builds
607
608
qyearsleyeab3c042016-08-24 09:18:28 -0700609def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000610 """Prints nicely result of fetch_try_jobs."""
611 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700612 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000613 return
614
615 # Make a copy, because we'll be modifying builds dictionary.
616 builds = builds.copy()
617 builder_names_cache = {}
618
619 def get_builder(b):
620 try:
621 return builder_names_cache[b['id']]
622 except KeyError:
623 try:
624 parameters = json.loads(b['parameters_json'])
625 name = parameters['builder_name']
626 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700627 print('WARNING: failed to get builder name for build %s: %s' % (
628 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000629 name = None
630 builder_names_cache[b['id']] = name
631 return name
632
633 def get_bucket(b):
634 bucket = b['bucket']
635 if bucket.startswith('master.'):
636 return bucket[len('master.'):]
637 return bucket
638
639 if options.print_master:
640 name_fmt = '%%-%ds %%-%ds' % (
641 max(len(str(get_bucket(b))) for b in builds.itervalues()),
642 max(len(str(get_builder(b))) for b in builds.itervalues()))
643 def get_name(b):
644 return name_fmt % (get_bucket(b), get_builder(b))
645 else:
646 name_fmt = '%%-%ds' % (
647 max(len(str(get_builder(b))) for b in builds.itervalues()))
648 def get_name(b):
649 return name_fmt % get_builder(b)
650
651 def sort_key(b):
652 return b['status'], b.get('result'), get_name(b), b.get('url')
653
654 def pop(title, f, color=None, **kwargs):
655 """Pop matching builds from `builds` dict and print them."""
656
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000657 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000658 colorize = str
659 else:
660 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
661
662 result = []
663 for b in builds.values():
664 if all(b.get(k) == v for k, v in kwargs.iteritems()):
665 builds.pop(b['id'])
666 result.append(b)
667 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700668 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000669 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700670 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000671
672 total = len(builds)
673 pop(status='COMPLETED', result='SUCCESS',
674 title='Successes:', color=Fore.GREEN,
675 f=lambda b: (get_name(b), b.get('url')))
676 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
677 title='Infra Failures:', color=Fore.MAGENTA,
678 f=lambda b: (get_name(b), b.get('url')))
679 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
680 title='Failures:', color=Fore.RED,
681 f=lambda b: (get_name(b), b.get('url')))
682 pop(status='COMPLETED', result='CANCELED',
683 title='Canceled:', color=Fore.MAGENTA,
684 f=lambda b: (get_name(b),))
685 pop(status='COMPLETED', result='FAILURE',
686 failure_reason='INVALID_BUILD_DEFINITION',
687 title='Wrong master/builder name:', color=Fore.MAGENTA,
688 f=lambda b: (get_name(b),))
689 pop(status='COMPLETED', result='FAILURE',
690 title='Other failures:',
691 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
692 pop(status='COMPLETED',
693 title='Other finished:',
694 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
695 pop(status='STARTED',
696 title='Started:', color=Fore.YELLOW,
697 f=lambda b: (get_name(b), b.get('url')))
698 pop(status='SCHEDULED',
699 title='Scheduled:',
700 f=lambda b: (get_name(b), 'id=%s' % b['id']))
701 # The last section is just in case buildbucket API changes OR there is a bug.
702 pop(title='Other:',
703 f=lambda b: (get_name(b), 'id=%s' % b['id']))
704 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700705 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000706
707
qyearsley53f48a12016-09-01 10:45:13 -0700708def write_try_results_json(output_file, builds):
709 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
710
711 The input |builds| dict is assumed to be generated by Buildbucket.
712 Buildbucket documentation: http://goo.gl/G0s101
713 """
714
715 def convert_build_dict(build):
716 return {
717 'buildbucket_id': build.get('id'),
718 'status': build.get('status'),
719 'result': build.get('result'),
720 'bucket': build.get('bucket'),
721 'builder_name': json.loads(
722 build.get('parameters_json', '{}')).get('builder_name'),
723 'failure_reason': build.get('failure_reason'),
724 'url': build.get('url'),
725 }
726
727 converted = []
728 for _, build in sorted(builds.items()):
729 converted.append(convert_build_dict(build))
730 write_json(output_file, converted)
731
732
iannucci@chromium.org79540052012-10-19 23:15:26 +0000733def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000734 """Prints statistics about the change to the user."""
735 # --no-ext-diff is broken in some versions of Git, so try to work around
736 # this by overriding the environment (but there is still a problem if the
737 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000738 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000739 if 'GIT_EXTERNAL_DIFF' in env:
740 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000741
742 if find_copies:
scottmgb84b5e32016-11-10 09:25:33 -0800743 similarity_options = ['-l100000', '-C%s' % similarity]
iannucci@chromium.org79540052012-10-19 23:15:26 +0000744 else:
745 similarity_options = ['-M%s' % similarity]
746
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000747 try:
748 stdout = sys.stdout.fileno()
749 except AttributeError:
750 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000751 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000752 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000753 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000754 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000755
756
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000757class BuildbucketResponseException(Exception):
758 pass
759
760
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000761class Settings(object):
762 def __init__(self):
763 self.default_server = None
764 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000765 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000766 self.tree_status_url = None
767 self.viewvc_url = None
768 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000769 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000770 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000771 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000772 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000773 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000774 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000775
776 def LazyUpdateIfNeeded(self):
777 """Updates the settings from a codereview.settings file, if available."""
778 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000779 # The only value that actually changes the behavior is
780 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000781 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000782 error_ok=True
783 ).strip().lower()
784
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000785 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000786 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000787 LoadCodereviewSettingsFromFile(cr_settings_file)
788 self.updated = True
789
790 def GetDefaultServerUrl(self, error_ok=False):
791 if not self.default_server:
792 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000793 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000794 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000795 if error_ok:
796 return self.default_server
797 if not self.default_server:
798 error_message = ('Could not find settings file. You must configure '
799 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000800 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000801 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000802 return self.default_server
803
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000804 @staticmethod
805 def GetRelativeRoot():
806 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000807
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000808 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000809 if self.root is None:
810 self.root = os.path.abspath(self.GetRelativeRoot())
811 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000812
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000813 def GetGitMirror(self, remote='origin'):
814 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000815 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000816 if not os.path.isdir(local_url):
817 return None
818 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
819 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100820 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 16:44:13 +0100821 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000822 if mirror.exists():
823 return mirror
824 return None
825
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000826 def GetTreeStatusUrl(self, error_ok=False):
827 if not self.tree_status_url:
828 error_message = ('You must configure your tree status URL by running '
829 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000830 self.tree_status_url = self._GetRietveldConfig(
831 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000832 return self.tree_status_url
833
834 def GetViewVCUrl(self):
835 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000836 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000837 return self.viewvc_url
838
Mark Mentovai57c47212017-03-09 11:14:09 -0500839 def GetBugLineFormat(self):
840 # rietveld.bug-line-format should have a %s where the list of bugs should
841 # go. This is a bit of a quirk, because normal people will always want the
842 # bug list to go right after a prefix like BUG= or Bug:. The %s format
843 # approach is used strictly because there isn't a great way to carry the
844 # desired space after Bug: all the way from codereview.settings to here
845 # without treating : specially or inventing a quoting scheme.
846 bug_line_format = self._GetRietveldConfig('bug-line-format', error_ok=True)
847 if not bug_line_format:
848 # TODO(tandrii): change this to 'Bug: %s' to be a proper Gerrit footer.
849 bug_line_format = 'BUG=%s'
850 return bug_line_format
851
rmistry@google.com90752582014-01-14 21:04:50 +0000852 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000853 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000854
rmistry@google.com78948ed2015-07-08 23:09:57 +0000855 def GetIsSkipDependencyUpload(self, branch_name):
856 """Returns true if specified branch should skip dep uploads."""
857 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
858 error_ok=True)
859
rmistry@google.com5626a922015-02-26 14:03:30 +0000860 def GetRunPostUploadHook(self):
861 run_post_upload_hook = self._GetRietveldConfig(
862 'run-post-upload-hook', error_ok=True)
863 return run_post_upload_hook == "True"
864
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000865 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000866 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000867
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000868 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000869 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000870
ukai@chromium.orge8077812012-02-03 03:41:46 +0000871 def GetIsGerrit(self):
872 """Return true if this repo is assosiated with gerrit code review system."""
873 if self.is_gerrit is None:
874 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
875 return self.is_gerrit
876
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000877 def GetSquashGerritUploads(self):
878 """Return true if uploads to Gerrit should be squashed by default."""
879 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700880 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
881 if self.squash_gerrit_uploads is None:
882 # Default is squash now (http://crbug.com/611892#c23).
883 self.squash_gerrit_uploads = not (
884 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
885 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000886 return self.squash_gerrit_uploads
887
tandriia60502f2016-06-20 02:01:53 -0700888 def GetSquashGerritUploadsOverride(self):
889 """Return True or False if codereview.settings should be overridden.
890
891 Returns None if no override has been defined.
892 """
893 # See also http://crbug.com/611892#c23
894 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
895 error_ok=True).strip()
896 if result == 'true':
897 return True
898 if result == 'false':
899 return False
900 return None
901
tandrii@chromium.org28253532016-04-14 13:46:56 +0000902 def GetGerritSkipEnsureAuthenticated(self):
903 """Return True if EnsureAuthenticated should not be done for Gerrit
904 uploads."""
905 if self.gerrit_skip_ensure_authenticated is None:
906 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000907 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000908 error_ok=True).strip() == 'true')
909 return self.gerrit_skip_ensure_authenticated
910
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000911 def GetGitEditor(self):
912 """Return the editor specified in the git config, or None if none is."""
913 if self.git_editor is None:
914 self.git_editor = self._GetConfig('core.editor', error_ok=True)
915 return self.git_editor or None
916
thestig@chromium.org44202a22014-03-11 19:22:18 +0000917 def GetLintRegex(self):
918 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
919 DEFAULT_LINT_REGEX)
920
921 def GetLintIgnoreRegex(self):
922 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
923 DEFAULT_LINT_IGNORE_REGEX)
924
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000925 def GetProject(self):
926 if not self.project:
927 self.project = self._GetRietveldConfig('project', error_ok=True)
928 return self.project
929
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000930 def _GetRietveldConfig(self, param, **kwargs):
931 return self._GetConfig('rietveld.' + param, **kwargs)
932
rmistry@google.com78948ed2015-07-08 23:09:57 +0000933 def _GetBranchConfig(self, branch_name, param, **kwargs):
934 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
935
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000936 def _GetConfig(self, param, **kwargs):
937 self.LazyUpdateIfNeeded()
938 return RunGit(['config', param], **kwargs).strip()
939
940
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100941@contextlib.contextmanager
942def _get_gerrit_project_config_file(remote_url):
943 """Context manager to fetch and store Gerrit's project.config from
944 refs/meta/config branch and store it in temp file.
945
946 Provides a temporary filename or None if there was error.
947 """
948 error, _ = RunGitWithCode([
949 'fetch', remote_url,
950 '+refs/meta/config:refs/git_cl/meta/config'])
951 if error:
952 # Ref doesn't exist or isn't accessible to current user.
953 print('WARNING: failed to fetch project config for %s: %s' %
954 (remote_url, error))
955 yield None
956 return
957
958 error, project_config_data = RunGitWithCode(
959 ['show', 'refs/git_cl/meta/config:project.config'])
960 if error:
961 print('WARNING: project.config file not found')
962 yield None
963 return
964
965 with gclient_utils.temporary_directory() as tempdir:
966 project_config_file = os.path.join(tempdir, 'project.config')
967 gclient_utils.FileWrite(project_config_file, project_config_data)
968 yield project_config_file
969
970
971def _is_git_numberer_enabled(remote_url, remote_ref):
972 """Returns True if Git Numberer is enabled on this ref."""
973 # TODO(tandrii): this should be deleted once repos below are 100% on Gerrit.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100974 KNOWN_PROJECTS_WHITELIST = [
975 'chromium/src',
976 'external/webrtc',
977 'v8/v8',
978 ]
979
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100980 assert remote_ref and remote_ref.startswith('refs/'), remote_ref
981 url_parts = urlparse.urlparse(remote_url)
982 project_name = url_parts.path.lstrip('/').rstrip('git./')
983 for known in KNOWN_PROJECTS_WHITELIST:
984 if project_name.endswith(known):
985 break
986 else:
987 # Early exit to avoid extra fetches for repos that aren't using Git
988 # Numberer.
989 return False
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100990
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100991 with _get_gerrit_project_config_file(remote_url) as project_config_file:
992 if project_config_file is None:
993 # Failed to fetch project.config, which shouldn't happen on open source
994 # repos KNOWN_PROJECTS_WHITELIST.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100995 return False
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100996 def get_opts(x):
997 code, out = RunGitWithCode(
998 ['config', '-f', project_config_file, '--get-all',
999 'plugin.git-numberer.validate-%s-refglob' % x])
1000 if code == 0:
1001 return out.strip().splitlines()
1002 return []
1003 enabled, disabled = map(get_opts, ['enabled', 'disabled'])
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001004
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01001005 logging.info('validator config enabled %s disabled %s refglobs for '
1006 '(this ref: %s)', enabled, disabled, remote_ref)
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +00001007
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01001008 def match_refglobs(refglobs):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001009 for refglob in refglobs:
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01001010 if remote_ref == refglob or fnmatch.fnmatch(remote_ref, refglob):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001011 return True
1012 return False
1013
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01001014 if match_refglobs(disabled):
1015 return False
1016 return match_refglobs(enabled)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001017
1018
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001019def ShortBranchName(branch):
1020 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001021 return branch.replace('refs/heads/', '', 1)
1022
1023
1024def GetCurrentBranchRef():
1025 """Returns branch ref (e.g., refs/heads/master) or None."""
1026 return RunGit(['symbolic-ref', 'HEAD'],
1027 stderr=subprocess2.VOID, error_ok=True).strip() or None
1028
1029
1030def GetCurrentBranch():
1031 """Returns current branch or None.
1032
1033 For refs/heads/* branches, returns just last part. For others, full ref.
1034 """
1035 branchref = GetCurrentBranchRef()
1036 if branchref:
1037 return ShortBranchName(branchref)
1038 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001039
1040
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001041class _CQState(object):
1042 """Enum for states of CL with respect to Commit Queue."""
1043 NONE = 'none'
1044 DRY_RUN = 'dry_run'
1045 COMMIT = 'commit'
1046
1047 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1048
1049
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001050class _ParsedIssueNumberArgument(object):
1051 def __init__(self, issue=None, patchset=None, hostname=None):
1052 self.issue = issue
1053 self.patchset = patchset
1054 self.hostname = hostname
1055
1056 @property
1057 def valid(self):
1058 return self.issue is not None
1059
1060
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001061def ParseIssueNumberArgument(arg):
1062 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1063 fail_result = _ParsedIssueNumberArgument()
1064
1065 if arg.isdigit():
1066 return _ParsedIssueNumberArgument(issue=int(arg))
1067 if not arg.startswith('http'):
1068 return fail_result
1069 url = gclient_utils.UpgradeToHttps(arg)
1070 try:
1071 parsed_url = urlparse.urlparse(url)
1072 except ValueError:
1073 return fail_result
1074 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
1075 tmp = cls.ParseIssueURL(parsed_url)
1076 if tmp is not None:
1077 return tmp
1078 return fail_result
1079
1080
Aaron Gablea45ee112016-11-22 15:14:38 -08001081class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001082 def __init__(self, issue, url):
1083 self.issue = issue
1084 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001085 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001086
1087 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001088 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001089 self.issue, self.url)
1090
1091
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001092class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001093 """Changelist works with one changelist in local branch.
1094
1095 Supports two codereview backends: Rietveld or Gerrit, selected at object
1096 creation.
1097
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001098 Notes:
1099 * Not safe for concurrent multi-{thread,process} use.
1100 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001101 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001102 """
1103
1104 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1105 """Create a new ChangeList instance.
1106
1107 If issue is given, the codereview must be given too.
1108
1109 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1110 Otherwise, it's decided based on current configuration of the local branch,
1111 with default being 'rietveld' for backwards compatibility.
1112 See _load_codereview_impl for more details.
1113
1114 **kwargs will be passed directly to codereview implementation.
1115 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001116 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001117 global settings
1118 if not settings:
1119 # Happens when git_cl.py is used as a utility library.
1120 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001121
1122 if issue:
1123 assert codereview, 'codereview must be known, if issue is known'
1124
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001125 self.branchref = branchref
1126 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001127 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001128 self.branch = ShortBranchName(self.branchref)
1129 else:
1130 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001131 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001132 self.lookedup_issue = False
1133 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001134 self.has_description = False
1135 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001136 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001137 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001138 self.cc = None
1139 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001140 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001141
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001142 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001143 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001144 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001145 assert self._codereview_impl
1146 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001147
1148 def _load_codereview_impl(self, codereview=None, **kwargs):
1149 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001150 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1151 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1152 self._codereview = codereview
1153 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001154 return
1155
1156 # Automatic selection based on issue number set for a current branch.
1157 # Rietveld takes precedence over Gerrit.
1158 assert not self.issue
1159 # Whether we find issue or not, we are doing the lookup.
1160 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001161 if self.GetBranch():
1162 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1163 issue = _git_get_branch_config_value(
1164 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1165 if issue:
1166 self._codereview = codereview
1167 self._codereview_impl = cls(self, **kwargs)
1168 self.issue = int(issue)
1169 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001170
1171 # No issue is set for this branch, so decide based on repo-wide settings.
1172 return self._load_codereview_impl(
1173 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1174 **kwargs)
1175
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001176 def IsGerrit(self):
1177 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001178
1179 def GetCCList(self):
1180 """Return the users cc'd on this CL.
1181
agable92bec4f2016-08-24 09:27:27 -07001182 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001183 """
1184 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001185 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001186 more_cc = ','.join(self.watchers)
1187 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1188 return self.cc
1189
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001190 def GetCCListWithoutDefault(self):
1191 """Return the users cc'd on this CL excluding default ones."""
1192 if self.cc is None:
1193 self.cc = ','.join(self.watchers)
1194 return self.cc
1195
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001196 def SetWatchers(self, watchers):
1197 """Set the list of email addresses that should be cc'd based on the changed
1198 files in this CL.
1199 """
1200 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001201
1202 def GetBranch(self):
1203 """Returns the short branch name, e.g. 'master'."""
1204 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001205 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001206 if not branchref:
1207 return None
1208 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001209 self.branch = ShortBranchName(self.branchref)
1210 return self.branch
1211
1212 def GetBranchRef(self):
1213 """Returns the full branch name, e.g. 'refs/heads/master'."""
1214 self.GetBranch() # Poke the lazy loader.
1215 return self.branchref
1216
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001217 def ClearBranch(self):
1218 """Clears cached branch data of this object."""
1219 self.branch = self.branchref = None
1220
tandrii5d48c322016-08-18 16:19:37 -07001221 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1222 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1223 kwargs['branch'] = self.GetBranch()
1224 return _git_get_branch_config_value(key, default, **kwargs)
1225
1226 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1227 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1228 assert self.GetBranch(), (
1229 'this CL must have an associated branch to %sset %s%s' %
1230 ('un' if value is None else '',
1231 key,
1232 '' if value is None else ' to %r' % value))
1233 kwargs['branch'] = self.GetBranch()
1234 return _git_set_branch_config_value(key, value, **kwargs)
1235
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001236 @staticmethod
1237 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001238 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001239 e.g. 'origin', 'refs/heads/master'
1240 """
1241 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001242 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1243
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001244 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001245 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001246 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001247 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1248 error_ok=True).strip()
1249 if upstream_branch:
1250 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001251 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001252 # Else, try to guess the origin remote.
1253 remote_branches = RunGit(['branch', '-r']).split()
1254 if 'origin/master' in remote_branches:
1255 # Fall back on origin/master if it exits.
1256 remote = 'origin'
1257 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001258 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001259 DieWithError(
1260 'Unable to determine default branch to diff against.\n'
1261 'Either pass complete "git diff"-style arguments, like\n'
1262 ' git cl upload origin/master\n'
1263 'or verify this branch is set up to track another \n'
1264 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001265
1266 return remote, upstream_branch
1267
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001268 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001269 upstream_branch = self.GetUpstreamBranch()
1270 if not BranchExists(upstream_branch):
1271 DieWithError('The upstream for the current branch (%s) does not exist '
1272 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001273 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001274 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001275
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001276 def GetUpstreamBranch(self):
1277 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001278 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001279 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001280 upstream_branch = upstream_branch.replace('refs/heads/',
1281 'refs/remotes/%s/' % remote)
1282 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1283 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001284 self.upstream_branch = upstream_branch
1285 return self.upstream_branch
1286
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001287 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001288 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001289 remote, branch = None, self.GetBranch()
1290 seen_branches = set()
1291 while branch not in seen_branches:
1292 seen_branches.add(branch)
1293 remote, branch = self.FetchUpstreamTuple(branch)
1294 branch = ShortBranchName(branch)
1295 if remote != '.' or branch.startswith('refs/remotes'):
1296 break
1297 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001298 remotes = RunGit(['remote'], error_ok=True).split()
1299 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001300 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001301 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001302 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001303 logging.warn('Could not determine which remote this change is '
1304 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001305 else:
1306 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001307 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001308 branch = 'HEAD'
1309 if branch.startswith('refs/remotes'):
1310 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001311 elif branch.startswith('refs/branch-heads/'):
1312 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001313 else:
1314 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001315 return self._remote
1316
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001317 def GitSanityChecks(self, upstream_git_obj):
1318 """Checks git repo status and ensures diff is from local commits."""
1319
sbc@chromium.org79706062015-01-14 21:18:12 +00001320 if upstream_git_obj is None:
1321 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001322 print('ERROR: unable to determine current branch (detached HEAD?)',
1323 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001324 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001325 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001326 return False
1327
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001328 # Verify the commit we're diffing against is in our current branch.
1329 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1330 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1331 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001332 print('ERROR: %s is not in the current branch. You may need to rebase '
1333 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001334 return False
1335
1336 # List the commits inside the diff, and verify they are all local.
1337 commits_in_diff = RunGit(
1338 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1339 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1340 remote_branch = remote_branch.strip()
1341 if code != 0:
1342 _, remote_branch = self.GetRemoteBranch()
1343
1344 commits_in_remote = RunGit(
1345 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1346
1347 common_commits = set(commits_in_diff) & set(commits_in_remote)
1348 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001349 print('ERROR: Your diff contains %d commits already in %s.\n'
1350 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1351 'the diff. If you are using a custom git flow, you can override'
1352 ' the reference used for this check with "git config '
1353 'gitcl.remotebranch <git-ref>".' % (
1354 len(common_commits), remote_branch, upstream_git_obj),
1355 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001356 return False
1357 return True
1358
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001359 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001360 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001361
1362 Returns None if it is not set.
1363 """
tandrii5d48c322016-08-18 16:19:37 -07001364 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001365
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001366 def GetRemoteUrl(self):
1367 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1368
1369 Returns None if there is no remote.
1370 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001371 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001372 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1373
1374 # If URL is pointing to a local directory, it is probably a git cache.
1375 if os.path.isdir(url):
1376 url = RunGit(['config', 'remote.%s.url' % remote],
1377 error_ok=True,
1378 cwd=url).strip()
1379 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001380
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001381 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001382 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001383 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001384 self.issue = self._GitGetBranchConfigValue(
1385 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001386 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001387 return self.issue
1388
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001389 def GetIssueURL(self):
1390 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001391 issue = self.GetIssue()
1392 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001393 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001394 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001395
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001396 def GetDescription(self, pretty=False, force=False):
1397 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001398 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001399 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001400 self.has_description = True
1401 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001402 # Set width to 72 columns + 2 space indent.
1403 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001404 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001405 lines = self.description.splitlines()
1406 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001407 return self.description
1408
1409 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001410 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001411 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001412 self.patchset = self._GitGetBranchConfigValue(
1413 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001414 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001415 return self.patchset
1416
1417 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001418 """Set this branch's patchset. If patchset=0, clears the patchset."""
1419 assert self.GetBranch()
1420 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001421 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001422 else:
1423 self.patchset = int(patchset)
1424 self._GitSetBranchConfigValue(
1425 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001426
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001427 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001428 """Set this branch's issue. If issue isn't given, clears the issue."""
1429 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001430 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001431 issue = int(issue)
1432 self._GitSetBranchConfigValue(
1433 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001434 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001435 codereview_server = self._codereview_impl.GetCodereviewServer()
1436 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001437 self._GitSetBranchConfigValue(
1438 self._codereview_impl.CodereviewServerConfigKey(),
1439 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001440 else:
tandrii5d48c322016-08-18 16:19:37 -07001441 # Reset all of these just to be clean.
1442 reset_suffixes = [
1443 'last-upload-hash',
1444 self._codereview_impl.IssueConfigKey(),
1445 self._codereview_impl.PatchsetConfigKey(),
1446 self._codereview_impl.CodereviewServerConfigKey(),
1447 ] + self._PostUnsetIssueProperties()
1448 for prop in reset_suffixes:
1449 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001450 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001451 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001452
dnjba1b0f32016-09-02 12:37:42 -07001453 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001454 if not self.GitSanityChecks(upstream_branch):
1455 DieWithError('\nGit sanity check failure')
1456
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001457 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001458 if not root:
1459 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001460 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001461
1462 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001463 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001464 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001465 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001466 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001467 except subprocess2.CalledProcessError:
1468 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001469 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001470 'This branch probably doesn\'t exist anymore. To reset the\n'
1471 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001472 ' git branch --set-upstream-to origin/master %s\n'
1473 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001474 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001475
maruel@chromium.org52424302012-08-29 15:14:30 +00001476 issue = self.GetIssue()
1477 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001478 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001479 description = self.GetDescription()
1480 else:
1481 # If the change was never uploaded, use the log messages of all commits
1482 # up to the branch point, as git cl upload will prefill the description
1483 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001484 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1485 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001486
1487 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001488 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001489 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001490 name,
1491 description,
1492 absroot,
1493 files,
1494 issue,
1495 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001496 author,
1497 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001498
dsansomee2d6fd92016-09-08 00:10:47 -07001499 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001500 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001501 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001502 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001503
1504 def RunHook(self, committing, may_prompt, verbose, change):
1505 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1506 try:
1507 return presubmit_support.DoPresubmitChecks(change, committing,
1508 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1509 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001510 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1511 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001512 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001513 DieWithError(
1514 ('%s\nMaybe your depot_tools is out of date?\n'
1515 'If all fails, contact maruel@') % e)
1516
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001517 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1518 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001519 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1520 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001521 else:
1522 # Assume url.
1523 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1524 urlparse.urlparse(issue_arg))
1525 if not parsed_issue_arg or not parsed_issue_arg.valid:
1526 DieWithError('Failed to parse issue argument "%s". '
1527 'Must be an issue number or a valid URL.' % issue_arg)
1528 return self._codereview_impl.CMDPatchWithParsedIssue(
1529 parsed_issue_arg, reject, nocommit, directory)
1530
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001531 def CMDUpload(self, options, git_diff_args, orig_args):
1532 """Uploads a change to codereview."""
1533 if git_diff_args:
1534 # TODO(ukai): is it ok for gerrit case?
1535 base_branch = git_diff_args[0]
1536 else:
1537 if self.GetBranch() is None:
1538 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1539
1540 # Default to diffing against common ancestor of upstream branch
1541 base_branch = self.GetCommonAncestorWithUpstream()
1542 git_diff_args = [base_branch, 'HEAD']
1543
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001544 # Fast best-effort checks to abort before running potentially
1545 # expensive hooks if uploading is likely to fail anyway. Passing these
1546 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001547 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001548 self._codereview_impl.EnsureCanUploadPatchset()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001549
1550 # Apply watchlists on upload.
1551 change = self.GetChange(base_branch, None)
1552 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1553 files = [f.LocalPath() for f in change.AffectedFiles()]
1554 if not options.bypass_watchlists:
1555 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1556
1557 if not options.bypass_hooks:
1558 if options.reviewers or options.tbr_owners:
1559 # Set the reviewer list now so that presubmit checks can access it.
1560 change_description = ChangeDescription(change.FullDescriptionText())
1561 change_description.update_reviewers(options.reviewers,
1562 options.tbr_owners,
1563 change)
1564 change.SetDescriptionText(change_description.description)
1565 hook_results = self.RunHook(committing=False,
1566 may_prompt=not options.force,
1567 verbose=options.verbose,
1568 change=change)
1569 if not hook_results.should_continue():
1570 return 1
1571 if not options.reviewers and hook_results.reviewers:
1572 options.reviewers = hook_results.reviewers.split(',')
1573
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001574 # TODO(tandrii): Checking local patchset against remote patchset is only
1575 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1576 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001577 latest_patchset = self.GetMostRecentPatchset()
1578 local_patchset = self.GetPatchset()
1579 if (latest_patchset and local_patchset and
1580 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001581 print('The last upload made from this repository was patchset #%d but '
1582 'the most recent patchset on the server is #%d.'
1583 % (local_patchset, latest_patchset))
1584 print('Uploading will still work, but if you\'ve uploaded to this '
1585 'issue from another machine or branch the patch you\'re '
1586 'uploading now might not include those changes.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001587 confirm_or_exit(action='upload')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001588
1589 print_stats(options.similarity, options.find_copies, git_diff_args)
1590 ret = self.CMDUploadChange(options, git_diff_args, change)
1591 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001592 if options.use_commit_queue:
1593 self.SetCQState(_CQState.COMMIT)
1594 elif options.cq_dry_run:
1595 self.SetCQState(_CQState.DRY_RUN)
1596
tandrii5d48c322016-08-18 16:19:37 -07001597 _git_set_branch_config_value('last-upload-hash',
1598 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001599 # Run post upload hooks, if specified.
1600 if settings.GetRunPostUploadHook():
1601 presubmit_support.DoPostUploadExecuter(
1602 change,
1603 self,
1604 settings.GetRoot(),
1605 options.verbose,
1606 sys.stdout)
1607
1608 # Upload all dependencies if specified.
1609 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001610 print()
1611 print('--dependencies has been specified.')
1612 print('All dependent local branches will be re-uploaded.')
1613 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001614 # Remove the dependencies flag from args so that we do not end up in a
1615 # loop.
1616 orig_args.remove('--dependencies')
1617 ret = upload_branch_deps(self, orig_args)
1618 return ret
1619
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001620 def SetCQState(self, new_state):
1621 """Update the CQ state for latest patchset.
1622
1623 Issue must have been already uploaded and known.
1624 """
1625 assert new_state in _CQState.ALL_STATES
1626 assert self.GetIssue()
1627 return self._codereview_impl.SetCQState(new_state)
1628
qyearsley1fdfcb62016-10-24 13:22:03 -07001629 def TriggerDryRun(self):
1630 """Triggers a dry run and prints a warning on failure."""
1631 # TODO(qyearsley): Either re-use this method in CMDset_commit
1632 # and CMDupload, or change CMDtry to trigger dry runs with
1633 # just SetCQState, and catch keyboard interrupt and other
1634 # errors in that method.
1635 try:
1636 self.SetCQState(_CQState.DRY_RUN)
1637 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1638 return 0
1639 except KeyboardInterrupt:
1640 raise
1641 except:
1642 print('WARNING: failed to trigger CQ Dry Run.\n'
1643 'Either:\n'
1644 ' * your project has no CQ\n'
1645 ' * you don\'t have permission to trigger Dry Run\n'
1646 ' * bug in this code (see stack trace below).\n'
1647 'Consider specifying which bots to trigger manually '
1648 'or asking your project owners for permissions '
1649 'or contacting Chrome Infrastructure team at '
1650 'https://www.chromium.org/infra\n\n')
1651 # Still raise exception so that stack trace is printed.
1652 raise
1653
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001654 # Forward methods to codereview specific implementation.
1655
1656 def CloseIssue(self):
1657 return self._codereview_impl.CloseIssue()
1658
1659 def GetStatus(self):
1660 return self._codereview_impl.GetStatus()
1661
1662 def GetCodereviewServer(self):
1663 return self._codereview_impl.GetCodereviewServer()
1664
tandriide281ae2016-10-12 06:02:30 -07001665 def GetIssueOwner(self):
1666 """Get owner from codereview, which may differ from this checkout."""
1667 return self._codereview_impl.GetIssueOwner()
1668
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001669 def GetApprovingReviewers(self):
1670 return self._codereview_impl.GetApprovingReviewers()
1671
1672 def GetMostRecentPatchset(self):
1673 return self._codereview_impl.GetMostRecentPatchset()
1674
tandriide281ae2016-10-12 06:02:30 -07001675 def CannotTriggerTryJobReason(self):
1676 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1677 return self._codereview_impl.CannotTriggerTryJobReason()
1678
tandrii8c5a3532016-11-04 07:52:02 -07001679 def GetTryjobProperties(self, patchset=None):
1680 """Returns dictionary of properties to launch tryjob."""
1681 return self._codereview_impl.GetTryjobProperties(patchset=patchset)
1682
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001683 def __getattr__(self, attr):
1684 # This is because lots of untested code accesses Rietveld-specific stuff
1685 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001686 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001687 # Note that child method defines __getattr__ as well, and forwards it here,
1688 # because _RietveldChangelistImpl is not cleaned up yet, and given
1689 # deprecation of Rietveld, it should probably be just removed.
1690 # Until that time, avoid infinite recursion by bypassing __getattr__
1691 # of implementation class.
1692 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001693
1694
1695class _ChangelistCodereviewBase(object):
1696 """Abstract base class encapsulating codereview specifics of a changelist."""
1697 def __init__(self, changelist):
1698 self._changelist = changelist # instance of Changelist
1699
1700 def __getattr__(self, attr):
1701 # Forward methods to changelist.
1702 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1703 # _RietveldChangelistImpl to avoid this hack?
1704 return getattr(self._changelist, attr)
1705
1706 def GetStatus(self):
1707 """Apply a rough heuristic to give a simple summary of an issue's review
1708 or CQ status, assuming adherence to a common workflow.
1709
1710 Returns None if no issue for this branch, or specific string keywords.
1711 """
1712 raise NotImplementedError()
1713
1714 def GetCodereviewServer(self):
1715 """Returns server URL without end slash, like "https://codereview.com"."""
1716 raise NotImplementedError()
1717
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001718 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001719 """Fetches and returns description from the codereview server."""
1720 raise NotImplementedError()
1721
tandrii5d48c322016-08-18 16:19:37 -07001722 @classmethod
1723 def IssueConfigKey(cls):
1724 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001725 raise NotImplementedError()
1726
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001727 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001728 def PatchsetConfigKey(cls):
1729 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001730 raise NotImplementedError()
1731
tandrii5d48c322016-08-18 16:19:37 -07001732 @classmethod
1733 def CodereviewServerConfigKey(cls):
1734 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001735 raise NotImplementedError()
1736
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001737 def _PostUnsetIssueProperties(self):
1738 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001739 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001740
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001741 def GetRieveldObjForPresubmit(self):
1742 # This is an unfortunate Rietveld-embeddedness in presubmit.
1743 # For non-Rietveld codereviews, this probably should return a dummy object.
1744 raise NotImplementedError()
1745
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001746 def GetGerritObjForPresubmit(self):
1747 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1748 return None
1749
dsansomee2d6fd92016-09-08 00:10:47 -07001750 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001751 """Update the description on codereview site."""
1752 raise NotImplementedError()
1753
1754 def CloseIssue(self):
1755 """Closes the issue."""
1756 raise NotImplementedError()
1757
1758 def GetApprovingReviewers(self):
1759 """Returns a list of reviewers approving the change.
1760
1761 Note: not necessarily committers.
1762 """
1763 raise NotImplementedError()
1764
1765 def GetMostRecentPatchset(self):
1766 """Returns the most recent patchset number from the codereview site."""
1767 raise NotImplementedError()
1768
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001769 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1770 directory):
1771 """Fetches and applies the issue.
1772
1773 Arguments:
1774 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1775 reject: if True, reject the failed patch instead of switching to 3-way
1776 merge. Rietveld only.
1777 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1778 only.
1779 directory: switch to directory before applying the patch. Rietveld only.
1780 """
1781 raise NotImplementedError()
1782
1783 @staticmethod
1784 def ParseIssueURL(parsed_url):
1785 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1786 failed."""
1787 raise NotImplementedError()
1788
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001789 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001790 """Best effort check that user is authenticated with codereview server.
1791
1792 Arguments:
1793 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001794 refresh: whether to attempt to refresh credentials. Ignored if not
1795 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001796 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001797 raise NotImplementedError()
1798
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001799 def EnsureCanUploadPatchset(self):
1800 """Best effort check that uploading isn't supposed to fail for predictable
1801 reasons.
1802
1803 This method should raise informative exception if uploading shouldn't
1804 proceed.
1805 """
1806 pass
1807
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001808 def CMDUploadChange(self, options, args, change):
1809 """Uploads a change to codereview."""
1810 raise NotImplementedError()
1811
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001812 def SetCQState(self, new_state):
1813 """Update the CQ state for latest patchset.
1814
1815 Issue must have been already uploaded and known.
1816 """
1817 raise NotImplementedError()
1818
tandriie113dfd2016-10-11 10:20:12 -07001819 def CannotTriggerTryJobReason(self):
1820 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1821 raise NotImplementedError()
1822
tandriide281ae2016-10-12 06:02:30 -07001823 def GetIssueOwner(self):
1824 raise NotImplementedError()
1825
tandrii8c5a3532016-11-04 07:52:02 -07001826 def GetTryjobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001827 raise NotImplementedError()
1828
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001829
1830class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001831 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001832 super(_RietveldChangelistImpl, self).__init__(changelist)
1833 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001834 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001835 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001836
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001837 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001838 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001839 self._props = None
1840 self._rpc_server = None
1841
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001842 def GetCodereviewServer(self):
1843 if not self._rietveld_server:
1844 # If we're on a branch then get the server potentially associated
1845 # with that branch.
1846 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001847 self._rietveld_server = gclient_utils.UpgradeToHttps(
1848 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001849 if not self._rietveld_server:
1850 self._rietveld_server = settings.GetDefaultServerUrl()
1851 return self._rietveld_server
1852
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001853 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001854 """Best effort check that user is authenticated with Rietveld server."""
1855 if self._auth_config.use_oauth2:
1856 authenticator = auth.get_authenticator_for_host(
1857 self.GetCodereviewServer(), self._auth_config)
1858 if not authenticator.has_cached_credentials():
1859 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001860 if refresh:
1861 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001862
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001863 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001864 issue = self.GetIssue()
1865 assert issue
1866 try:
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001867 return self.RpcServer().get_description(issue, force=force).strip()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001868 except urllib2.HTTPError as e:
1869 if e.code == 404:
1870 DieWithError(
1871 ('\nWhile fetching the description for issue %d, received a '
1872 '404 (not found)\n'
1873 'error. It is likely that you deleted this '
1874 'issue on the server. If this is the\n'
1875 'case, please run\n\n'
1876 ' git cl issue 0\n\n'
1877 'to clear the association with the deleted issue. Then run '
1878 'this command again.') % issue)
1879 else:
1880 DieWithError(
1881 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1882 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001883 print('Warning: Failed to retrieve CL description due to network '
1884 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001885 return ''
1886
1887 def GetMostRecentPatchset(self):
1888 return self.GetIssueProperties()['patchsets'][-1]
1889
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001890 def GetIssueProperties(self):
1891 if self._props is None:
1892 issue = self.GetIssue()
1893 if not issue:
1894 self._props = {}
1895 else:
1896 self._props = self.RpcServer().get_issue_properties(issue, True)
1897 return self._props
1898
tandriie113dfd2016-10-11 10:20:12 -07001899 def CannotTriggerTryJobReason(self):
1900 props = self.GetIssueProperties()
1901 if not props:
1902 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1903 if props.get('closed'):
1904 return 'CL %s is closed' % self.GetIssue()
1905 if props.get('private'):
1906 return 'CL %s is private' % self.GetIssue()
1907 return None
1908
tandrii8c5a3532016-11-04 07:52:02 -07001909 def GetTryjobProperties(self, patchset=None):
1910 """Returns dictionary of properties to launch tryjob."""
1911 project = (self.GetIssueProperties() or {}).get('project')
1912 return {
1913 'issue': self.GetIssue(),
1914 'patch_project': project,
1915 'patch_storage': 'rietveld',
1916 'patchset': patchset or self.GetPatchset(),
1917 'rietveld': self.GetCodereviewServer(),
1918 }
1919
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001920 def GetApprovingReviewers(self):
1921 return get_approving_reviewers(self.GetIssueProperties())
1922
tandriide281ae2016-10-12 06:02:30 -07001923 def GetIssueOwner(self):
1924 return (self.GetIssueProperties() or {}).get('owner_email')
1925
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001926 def AddComment(self, message):
1927 return self.RpcServer().add_comment(self.GetIssue(), message)
1928
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001929 def GetStatus(self):
1930 """Apply a rough heuristic to give a simple summary of an issue's review
1931 or CQ status, assuming adherence to a common workflow.
1932
1933 Returns None if no issue for this branch, or one of the following keywords:
1934 * 'error' - error from review tool (including deleted issues)
1935 * 'unsent' - not sent for review
1936 * 'waiting' - waiting for review
1937 * 'reply' - waiting for owner to reply to review
1938 * 'lgtm' - LGTM from at least one approved reviewer
1939 * 'commit' - in the commit queue
1940 * 'closed' - closed
1941 """
1942 if not self.GetIssue():
1943 return None
1944
1945 try:
1946 props = self.GetIssueProperties()
1947 except urllib2.HTTPError:
1948 return 'error'
1949
1950 if props.get('closed'):
1951 # Issue is closed.
1952 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001953 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001954 # Issue is in the commit queue.
1955 return 'commit'
1956
1957 try:
1958 reviewers = self.GetApprovingReviewers()
1959 except urllib2.HTTPError:
1960 return 'error'
1961
1962 if reviewers:
1963 # Was LGTM'ed.
1964 return 'lgtm'
1965
1966 messages = props.get('messages') or []
1967
tandrii9d2c7a32016-06-22 03:42:45 -07001968 # Skip CQ messages that don't require owner's action.
1969 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1970 if 'Dry run:' in messages[-1]['text']:
1971 messages.pop()
1972 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1973 # This message always follows prior messages from CQ,
1974 # so skip this too.
1975 messages.pop()
1976 else:
1977 # This is probably a CQ messages warranting user attention.
1978 break
1979
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001980 if not messages:
1981 # No message was sent.
1982 return 'unsent'
1983 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001984 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001985 return 'reply'
1986 return 'waiting'
1987
dsansomee2d6fd92016-09-08 00:10:47 -07001988 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001989 self.RpcServer().update_description(self.GetIssue(), description)
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001990
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001991 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001992 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001993
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001994 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07001995 return self.SetFlags({flag: value})
1996
1997 def SetFlags(self, flags):
1998 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07001999 """
phajdan.jr68598232016-08-10 03:28:28 -07002000 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002001 try:
tandrii4b233bd2016-07-06 03:50:29 -07002002 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002003 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002004 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002005 if e.code == 404:
2006 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2007 if e.code == 403:
2008 DieWithError(
2009 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002010 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002011 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002012
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002013 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002014 """Returns an upload.RpcServer() to access this review's rietveld instance.
2015 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002016 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002017 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002018 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01002019 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002020 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002021
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002022 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002023 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002024 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002025
tandrii5d48c322016-08-18 16:19:37 -07002026 @classmethod
2027 def PatchsetConfigKey(cls):
2028 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002029
tandrii5d48c322016-08-18 16:19:37 -07002030 @classmethod
2031 def CodereviewServerConfigKey(cls):
2032 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002033
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002034 def GetRieveldObjForPresubmit(self):
2035 return self.RpcServer()
2036
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002037 def SetCQState(self, new_state):
2038 props = self.GetIssueProperties()
2039 if props.get('private'):
2040 DieWithError('Cannot set-commit on private issue')
2041
2042 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002043 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002044 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002045 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002046 else:
tandrii4b233bd2016-07-06 03:50:29 -07002047 assert new_state == _CQState.DRY_RUN
2048 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002049
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002050 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2051 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002052 # PatchIssue should never be called with a dirty tree. It is up to the
2053 # caller to check this, but just in case we assert here since the
2054 # consequences of the caller not checking this could be dire.
2055 assert(not git_common.is_dirty_git_tree('apply'))
2056 assert(parsed_issue_arg.valid)
2057 self._changelist.issue = parsed_issue_arg.issue
2058 if parsed_issue_arg.hostname:
2059 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2060
skobes6468b902016-10-24 08:45:10 -07002061 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2062 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2063 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002064 try:
skobes6468b902016-10-24 08:45:10 -07002065 scm_obj.apply_patch(patchset_object)
2066 except Exception as e:
2067 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002068 return 1
2069
2070 # If we had an issue, commit the current state and register the issue.
2071 if not nocommit:
2072 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2073 'patch from issue %(i)s at patchset '
2074 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2075 % {'i': self.GetIssue(), 'p': patchset})])
2076 self.SetIssue(self.GetIssue())
2077 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07002078 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002079 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002080 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002081 return 0
2082
2083 @staticmethod
2084 def ParseIssueURL(parsed_url):
2085 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2086 return None
wychen3c1c1722016-08-04 11:46:36 -07002087 # Rietveld patch: https://domain/<number>/#ps<patchset>
2088 match = re.match(r'/(\d+)/$', parsed_url.path)
2089 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2090 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002091 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002092 issue=int(match.group(1)),
2093 patchset=int(match2.group(1)),
2094 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002095 # Typical url: https://domain/<issue_number>[/[other]]
2096 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2097 if match:
skobes6468b902016-10-24 08:45:10 -07002098 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002099 issue=int(match.group(1)),
2100 hostname=parsed_url.netloc)
2101 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2102 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2103 if match:
skobes6468b902016-10-24 08:45:10 -07002104 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002105 issue=int(match.group(1)),
2106 patchset=int(match.group(2)),
skobes6468b902016-10-24 08:45:10 -07002107 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002108 return None
2109
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002110 def CMDUploadChange(self, options, args, change):
2111 """Upload the patch to Rietveld."""
2112 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2113 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002114 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2115 if options.emulate_svn_auto_props:
2116 upload_args.append('--emulate_svn_auto_props')
2117
2118 change_desc = None
2119
2120 if options.email is not None:
2121 upload_args.extend(['--email', options.email])
2122
2123 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002124 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002125 upload_args.extend(['--title', options.title])
2126 if options.message:
2127 upload_args.extend(['--message', options.message])
2128 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002129 print('This branch is associated with issue %s. '
2130 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002131 else:
nodirca166002016-06-27 10:59:51 -07002132 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002133 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002134 if options.message:
2135 message = options.message
2136 else:
2137 message = CreateDescriptionFromLog(args)
2138 if options.title:
2139 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002140 change_desc = ChangeDescription(message)
2141 if options.reviewers or options.tbr_owners:
2142 change_desc.update_reviewers(options.reviewers,
2143 options.tbr_owners,
2144 change)
2145 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002146 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002147
2148 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002149 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002150 return 1
2151
2152 upload_args.extend(['--message', change_desc.description])
2153 if change_desc.get_reviewers():
2154 upload_args.append('--reviewers=%s' % ','.join(
2155 change_desc.get_reviewers()))
2156 if options.send_mail:
2157 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002158 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002159 upload_args.append('--send_mail')
2160
2161 # We check this before applying rietveld.private assuming that in
2162 # rietveld.cc only addresses which we can send private CLs to are listed
2163 # if rietveld.private is set, and so we should ignore rietveld.cc only
2164 # when --private is specified explicitly on the command line.
2165 if options.private:
2166 logging.warn('rietveld.cc is ignored since private flag is specified. '
2167 'You need to review and add them manually if necessary.')
2168 cc = self.GetCCListWithoutDefault()
2169 else:
2170 cc = self.GetCCList()
2171 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002172 if change_desc.get_cced():
2173 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002174 if cc:
2175 upload_args.extend(['--cc', cc])
2176
2177 if options.private or settings.GetDefaultPrivateFlag() == "True":
2178 upload_args.append('--private')
2179
2180 upload_args.extend(['--git_similarity', str(options.similarity)])
2181 if not options.find_copies:
2182 upload_args.extend(['--git_no_find_copies'])
2183
2184 # Include the upstream repo's URL in the change -- this is useful for
2185 # projects that have their source spread across multiple repos.
2186 remote_url = self.GetGitBaseUrlFromConfig()
2187 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002188 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2189 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2190 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002191 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002192 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002193 target_ref = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002194 if target_ref:
2195 upload_args.extend(['--target_ref', target_ref])
2196
2197 # Look for dependent patchsets. See crbug.com/480453 for more details.
2198 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2199 upstream_branch = ShortBranchName(upstream_branch)
2200 if remote is '.':
2201 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002202 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002203 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002204 print()
2205 print('Skipping dependency patchset upload because git config '
2206 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2207 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002208 else:
2209 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002210 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002211 auth_config=auth_config)
2212 branch_cl_issue_url = branch_cl.GetIssueURL()
2213 branch_cl_issue = branch_cl.GetIssue()
2214 branch_cl_patchset = branch_cl.GetPatchset()
2215 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2216 upload_args.extend(
2217 ['--depends_on_patchset', '%s:%s' % (
2218 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002219 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002220 '\n'
2221 'The current branch (%s) is tracking a local branch (%s) with '
2222 'an associated CL.\n'
2223 'Adding %s/#ps%s as a dependency patchset.\n'
2224 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2225 branch_cl_patchset))
2226
2227 project = settings.GetProject()
2228 if project:
2229 upload_args.extend(['--project', project])
2230
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002231 try:
2232 upload_args = ['upload'] + upload_args + args
2233 logging.info('upload.RealMain(%s)', upload_args)
2234 issue, patchset = upload.RealMain(upload_args)
2235 issue = int(issue)
2236 patchset = int(patchset)
2237 except KeyboardInterrupt:
2238 sys.exit(1)
2239 except:
2240 # If we got an exception after the user typed a description for their
2241 # change, back up the description before re-raising.
2242 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002243 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002244 raise
2245
2246 if not self.GetIssue():
2247 self.SetIssue(issue)
2248 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002249 return 0
2250
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002251
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002252class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002253 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002254 # auth_config is Rietveld thing, kept here to preserve interface only.
2255 super(_GerritChangelistImpl, self).__init__(changelist)
2256 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002257 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002258 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002259 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002260 # Map from change number (issue) to its detail cache.
2261 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002262
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002263 if codereview_host is not None:
2264 assert not codereview_host.startswith('https://'), codereview_host
2265 self._gerrit_host = codereview_host
2266 self._gerrit_server = 'https://%s' % codereview_host
2267
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002268 def _GetGerritHost(self):
2269 # Lazy load of configs.
2270 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002271 if self._gerrit_host and '.' not in self._gerrit_host:
2272 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2273 # This happens for internal stuff http://crbug.com/614312.
2274 parsed = urlparse.urlparse(self.GetRemoteUrl())
2275 if parsed.scheme == 'sso':
2276 print('WARNING: using non https URLs for remote is likely broken\n'
2277 ' Your current remote is: %s' % self.GetRemoteUrl())
2278 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2279 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002280 return self._gerrit_host
2281
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002282 def _GetGitHost(self):
2283 """Returns git host to be used when uploading change to Gerrit."""
2284 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2285
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002286 def GetCodereviewServer(self):
2287 if not self._gerrit_server:
2288 # If we're on a branch then get the server potentially associated
2289 # with that branch.
2290 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002291 self._gerrit_server = self._GitGetBranchConfigValue(
2292 self.CodereviewServerConfigKey())
2293 if self._gerrit_server:
2294 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002295 if not self._gerrit_server:
2296 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2297 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002298 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002299 parts[0] = parts[0] + '-review'
2300 self._gerrit_host = '.'.join(parts)
2301 self._gerrit_server = 'https://%s' % self._gerrit_host
2302 return self._gerrit_server
2303
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002304 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002305 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002306 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002307
tandrii5d48c322016-08-18 16:19:37 -07002308 @classmethod
2309 def PatchsetConfigKey(cls):
2310 return 'gerritpatchset'
2311
2312 @classmethod
2313 def CodereviewServerConfigKey(cls):
2314 return 'gerritserver'
2315
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002316 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002317 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002318 if settings.GetGerritSkipEnsureAuthenticated():
2319 # For projects with unusual authentication schemes.
2320 # See http://crbug.com/603378.
2321 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002322 # Lazy-loader to identify Gerrit and Git hosts.
2323 if gerrit_util.GceAuthenticator.is_gce():
2324 return
2325 self.GetCodereviewServer()
2326 git_host = self._GetGitHost()
2327 assert self._gerrit_server and self._gerrit_host
2328 cookie_auth = gerrit_util.CookiesAuthenticator()
2329
2330 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2331 git_auth = cookie_auth.get_auth_header(git_host)
2332 if gerrit_auth and git_auth:
2333 if gerrit_auth == git_auth:
2334 return
2335 print((
2336 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2337 ' Check your %s or %s file for credentials of hosts:\n'
2338 ' %s\n'
2339 ' %s\n'
2340 ' %s') %
2341 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2342 git_host, self._gerrit_host,
2343 cookie_auth.get_new_password_message(git_host)))
2344 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002345 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002346 return
2347 else:
2348 missing = (
2349 [] if gerrit_auth else [self._gerrit_host] +
2350 [] if git_auth else [git_host])
2351 DieWithError('Credentials for the following hosts are required:\n'
2352 ' %s\n'
2353 'These are read from %s (or legacy %s)\n'
2354 '%s' % (
2355 '\n '.join(missing),
2356 cookie_auth.get_gitcookies_path(),
2357 cookie_auth.get_netrc_path(),
2358 cookie_auth.get_new_password_message(git_host)))
2359
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002360 def EnsureCanUploadPatchset(self):
2361 """Best effort check that uploading isn't supposed to fail for predictable
2362 reasons.
2363
2364 This method should raise informative exception if uploading shouldn't
2365 proceed.
2366 """
2367 if not self.GetIssue():
2368 return
2369
2370 # Warm change details cache now to avoid RPCs later, reducing latency for
2371 # developers.
2372 self.FetchDescription()
2373
2374 status = self._GetChangeDetail()['status']
2375 if status in ('MERGED', 'ABANDONED'):
2376 DieWithError('Change %s has been %s, new uploads are not allowed' %
2377 (self.GetIssueURL(),
2378 'submitted' if status == 'MERGED' else 'abandoned'))
2379
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002380 def _PostUnsetIssueProperties(self):
2381 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002382 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002383
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002384 def GetRieveldObjForPresubmit(self):
2385 class ThisIsNotRietveldIssue(object):
2386 def __nonzero__(self):
2387 # This is a hack to make presubmit_support think that rietveld is not
2388 # defined, yet still ensure that calls directly result in a decent
2389 # exception message below.
2390 return False
2391
2392 def __getattr__(self, attr):
2393 print(
2394 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2395 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2396 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2397 'or use Rietveld for codereview.\n'
2398 'See also http://crbug.com/579160.' % attr)
2399 raise NotImplementedError()
2400 return ThisIsNotRietveldIssue()
2401
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002402 def GetGerritObjForPresubmit(self):
2403 return presubmit_support.GerritAccessor(self._GetGerritHost())
2404
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002405 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002406 """Apply a rough heuristic to give a simple summary of an issue's review
2407 or CQ status, assuming adherence to a common workflow.
2408
2409 Returns None if no issue for this branch, or one of the following keywords:
2410 * 'error' - error from review tool (including deleted issues)
2411 * 'unsent' - no reviewers added
2412 * 'waiting' - waiting for review
2413 * 'reply' - waiting for owner to reply to review
Quinten Yearsley442fb642016-12-15 15:38:27 -08002414 * 'not lgtm' - Code-Review disapproval from at least one valid reviewer
tandriic2405f52016-10-10 08:13:15 -07002415 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002416 * 'commit' - in the commit queue
2417 * 'closed' - abandoned
2418 """
2419 if not self.GetIssue():
2420 return None
2421
2422 try:
2423 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002424 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002425 return 'error'
2426
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002427 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002428 return 'closed'
2429
2430 cq_label = data['labels'].get('Commit-Queue', {})
2431 if cq_label:
rmistryc9ebbd22016-10-14 12:35:54 -07002432 votes = cq_label.get('all', [])
2433 highest_vote = 0
2434 for v in votes:
2435 highest_vote = max(highest_vote, v.get('value', 0))
2436 vote_value = str(highest_vote)
2437 if vote_value != '0':
2438 # Add a '+' if the value is not 0 to match the values in the label.
2439 # The cq_label does not have negatives.
2440 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002441 vote_text = cq_label.get('values', {}).get(vote_value, '')
2442 if vote_text.lower() == 'commit':
2443 return 'commit'
2444
2445 lgtm_label = data['labels'].get('Code-Review', {})
2446 if lgtm_label:
2447 if 'rejected' in lgtm_label:
2448 return 'not lgtm'
2449 if 'approved' in lgtm_label:
2450 return 'lgtm'
2451
2452 if not data.get('reviewers', {}).get('REVIEWER', []):
2453 return 'unsent'
2454
2455 messages = data.get('messages', [])
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002456 owner = data['owner'].get('_account_id')
2457 while messages:
2458 last_message_author = messages.pop().get('author', {})
2459 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2460 # Ignore replies from CQ.
2461 continue
Andrii Shyshkalov7afd1642017-01-29 16:07:15 +01002462 if last_message_author.get('_account_id') != owner:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002463 # Some reply from non-owner.
2464 return 'reply'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002465 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002466
2467 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002468 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002469 return data['revisions'][data['current_revision']]['_number']
2470
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002471 def FetchDescription(self, force=False):
2472 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2473 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002474 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002475 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002476
dsansomee2d6fd92016-09-08 00:10:47 -07002477 def UpdateDescriptionRemote(self, description, force=False):
2478 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2479 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002480 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002481 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002482 'unpublished edit. Either publish the edit in the Gerrit web UI '
2483 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002484
2485 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2486 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002487 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002488 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002489
2490 def CloseIssue(self):
2491 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2492
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002493 def GetApprovingReviewers(self):
2494 """Returns a list of reviewers approving the change.
2495
2496 Note: not necessarily committers.
2497 """
2498 raise NotImplementedError()
2499
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002500 def SubmitIssue(self, wait_for_merge=True):
2501 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2502 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002503
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002504 def _GetChangeDetail(self, options=None, issue=None,
2505 no_cache=False):
2506 """Returns details of the issue by querying Gerrit and caching results.
2507
2508 If fresh data is needed, set no_cache=True which will clear cache and
2509 thus new data will be fetched from Gerrit.
2510 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002511 options = options or []
2512 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002513 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002514
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002515 # Optimization to avoid multiple RPCs:
2516 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2517 'CURRENT_COMMIT' not in options):
2518 options.append('CURRENT_COMMIT')
2519
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002520 # Normalize issue and options for consistent keys in cache.
2521 issue = str(issue)
2522 options = [o.upper() for o in options]
2523
2524 # Check in cache first unless no_cache is True.
2525 if no_cache:
2526 self._detail_cache.pop(issue, None)
2527 else:
2528 options_set = frozenset(options)
2529 for cached_options_set, data in self._detail_cache.get(issue, []):
2530 # Assumption: data fetched before with extra options is suitable
2531 # for return for a smaller set of options.
2532 # For example, if we cached data for
2533 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2534 # and request is for options=[CURRENT_REVISION],
2535 # THEN we can return prior cached data.
2536 if options_set.issubset(cached_options_set):
2537 return data
2538
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002539 try:
2540 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2541 options, ignore_404=False)
2542 except gerrit_util.GerritError as e:
2543 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002544 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002545 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002546
2547 self._detail_cache.setdefault(issue, []).append((frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002548 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002549
agable32978d92016-11-01 12:55:02 -07002550 def _GetChangeCommit(self, issue=None):
2551 issue = issue or self.GetIssue()
2552 assert issue, 'issue is required to query Gerrit'
2553 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2554 if not data:
Aaron Gablea45ee112016-11-22 15:14:38 -08002555 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
agable32978d92016-11-01 12:55:02 -07002556 return data
2557
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002558 def CMDLand(self, force, bypass_hooks, verbose):
2559 if git_common.is_dirty_git_tree('land'):
2560 return 1
tandriid60367b2016-06-22 05:25:12 -07002561 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2562 if u'Commit-Queue' in detail.get('labels', {}):
2563 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002564 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2565 'which can test and land changes for you. '
2566 'Are you sure you wish to bypass it?\n',
2567 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002568
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002569 differs = True
tandriic4344b52016-08-29 06:04:54 -07002570 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002571 # Note: git diff outputs nothing if there is no diff.
2572 if not last_upload or RunGit(['diff', last_upload]).strip():
2573 print('WARNING: some changes from local branch haven\'t been uploaded')
2574 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002575 if detail['current_revision'] == last_upload:
2576 differs = False
2577 else:
2578 print('WARNING: local branch contents differ from latest uploaded '
2579 'patchset')
2580 if differs:
2581 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002582 confirm_or_exit(
2583 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2584 action='submit')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002585 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2586 elif not bypass_hooks:
2587 hook_results = self.RunHook(
2588 committing=True,
2589 may_prompt=not force,
2590 verbose=verbose,
2591 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2592 if not hook_results.should_continue():
2593 return 1
2594
2595 self.SubmitIssue(wait_for_merge=True)
2596 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002597 links = self._GetChangeCommit().get('web_links', [])
2598 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002599 if link.get('name') == 'gitiles' and link.get('url'):
agable32978d92016-11-01 12:55:02 -07002600 print('Landed as %s' % link.get('url'))
2601 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002602 return 0
2603
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002604 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2605 directory):
2606 assert not reject
2607 assert not nocommit
2608 assert not directory
2609 assert parsed_issue_arg.valid
2610
2611 self._changelist.issue = parsed_issue_arg.issue
2612
2613 if parsed_issue_arg.hostname:
2614 self._gerrit_host = parsed_issue_arg.hostname
2615 self._gerrit_server = 'https://%s' % self._gerrit_host
2616
tandriic2405f52016-10-10 08:13:15 -07002617 try:
2618 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002619 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002620 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002621
2622 if not parsed_issue_arg.patchset:
2623 # Use current revision by default.
2624 revision_info = detail['revisions'][detail['current_revision']]
2625 patchset = int(revision_info['_number'])
2626 else:
2627 patchset = parsed_issue_arg.patchset
2628 for revision_info in detail['revisions'].itervalues():
2629 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2630 break
2631 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002632 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002633 (parsed_issue_arg.patchset, self.GetIssue()))
2634
2635 fetch_info = revision_info['fetch']['http']
2636 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2637 RunGit(['cherry-pick', 'FETCH_HEAD'])
2638 self.SetIssue(self.GetIssue())
2639 self.SetPatchset(patchset)
Aaron Gablea45ee112016-11-22 15:14:38 -08002640 print('Committed patch for change %i patchset %i locally' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002641 (self.GetIssue(), self.GetPatchset()))
2642 return 0
2643
2644 @staticmethod
2645 def ParseIssueURL(parsed_url):
2646 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2647 return None
2648 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2649 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2650 # Short urls like https://domain/<issue_number> can be used, but don't allow
2651 # specifying the patchset (you'd 404), but we allow that here.
2652 if parsed_url.path == '/':
2653 part = parsed_url.fragment
2654 else:
2655 part = parsed_url.path
2656 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2657 if match:
2658 return _ParsedIssueNumberArgument(
2659 issue=int(match.group(2)),
2660 patchset=int(match.group(4)) if match.group(4) else None,
2661 hostname=parsed_url.netloc)
2662 return None
2663
tandrii16e0b4e2016-06-07 10:34:28 -07002664 def _GerritCommitMsgHookCheck(self, offer_removal):
2665 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2666 if not os.path.exists(hook):
2667 return
2668 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2669 # custom developer made one.
2670 data = gclient_utils.FileRead(hook)
2671 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2672 return
2673 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002674 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002675 'and may interfere with it in subtle ways.\n'
2676 'We recommend you remove the commit-msg hook.')
2677 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002678 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002679 gclient_utils.rm_file_or_tree(hook)
2680 print('Gerrit commit-msg hook removed.')
2681 else:
2682 print('OK, will keep Gerrit commit-msg hook in place.')
2683
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002684 def CMDUploadChange(self, options, args, change):
2685 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002686 if options.squash and options.no_squash:
2687 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002688
2689 if not options.squash and not options.no_squash:
2690 # Load default for user, repo, squash=true, in this order.
2691 options.squash = settings.GetSquashGerritUploads()
2692 elif options.no_squash:
2693 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002694
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002695 # We assume the remote called "origin" is the one we want.
2696 # It is probably not worthwhile to support different workflows.
2697 gerrit_remote = 'origin'
2698
2699 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002700 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002701
Aaron Gableb56ad332017-01-06 15:24:31 -08002702 # This may be None; default fallback value is determined in logic below.
2703 title = options.title
Aaron Gable856585d2017-01-23 15:32:00 -08002704 automatic_title = False
Aaron Gableb56ad332017-01-06 15:24:31 -08002705
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002706 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002707 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002708 if self.GetIssue():
2709 # Try to get the message from a previous upload.
2710 message = self.GetDescription()
2711 if not message:
2712 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002713 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002714 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002715 if not title:
2716 default_title = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
2717 title = ask_for_data(
2718 'Title for patchset [%s]: ' % default_title) or default_title
Aaron Gable856585d2017-01-23 15:32:00 -08002719 if title == default_title:
2720 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002721 change_id = self._GetChangeDetail()['change_id']
2722 while True:
2723 footer_change_ids = git_footers.get_footer_change_id(message)
2724 if footer_change_ids == [change_id]:
2725 break
2726 if not footer_change_ids:
2727 message = git_footers.add_footer_change_id(message, change_id)
Aaron Gablea45ee112016-11-22 15:14:38 -08002728 print('WARNING: appended missing Change-Id to change description')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002729 continue
2730 # There is already a valid footer but with different or several ids.
2731 # Doing this automatically is non-trivial as we don't want to lose
2732 # existing other footers, yet we want to append just 1 desired
2733 # Change-Id. Thus, just create a new footer, but let user verify the
2734 # new description.
2735 message = '%s\n\nChange-Id: %s' % (message, change_id)
2736 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002737 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002738 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002739 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002740 'Please, check the proposed correction to the description, '
2741 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2742 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2743 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002744 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002745 if not options.force:
2746 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002747 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002748 message = change_desc.description
2749 if not message:
2750 DieWithError("Description is empty. Aborting...")
2751 # Continue the while loop.
2752 # Sanity check of this code - we should end up with proper message
2753 # footer.
2754 assert [change_id] == git_footers.get_footer_change_id(message)
2755 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002756 else: # if not self.GetIssue()
2757 if options.message:
2758 message = options.message
2759 else:
2760 message = CreateDescriptionFromLog(args)
2761 if options.title:
2762 message = options.title + '\n\n' + message
2763 change_desc = ChangeDescription(message)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002764 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002765 change_desc.prompt(bug=options.bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002766 # On first upload, patchset title is always this string, while
2767 # --title flag gets converted to first line of message.
2768 title = 'Initial upload'
Aaron Gable856585d2017-01-23 15:32:00 -08002769 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002770 if not change_desc.description:
2771 DieWithError("Description is empty. Aborting...")
2772 message = change_desc.description
2773 change_ids = git_footers.get_footer_change_id(message)
2774 if len(change_ids) > 1:
2775 DieWithError('too many Change-Id footers, at most 1 allowed.')
2776 if not change_ids:
2777 # Generate the Change-Id automatically.
2778 message = git_footers.add_footer_change_id(
2779 message, GenerateGerritChangeId(message))
2780 change_desc.set_description(message)
2781 change_ids = git_footers.get_footer_change_id(message)
2782 assert len(change_ids) == 1
2783 change_id = change_ids[0]
2784
2785 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2786 if remote is '.':
2787 # If our upstream branch is local, we base our squashed commit on its
2788 # squashed version.
2789 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2790 # Check the squashed hash of the parent.
2791 parent = RunGit(['config',
2792 'branch.%s.gerritsquashhash' % upstream_branch_name],
2793 error_ok=True).strip()
2794 # Verify that the upstream branch has been uploaded too, otherwise
2795 # Gerrit will create additional CLs when uploading.
2796 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2797 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002798 DieWithError(
Aaron Gable48746652017-01-06 11:49:21 -08002799 '\nUpload upstream branch %s first.\n'
2800 'It is likely that this branch has been rebased since its last '
2801 'upload, so you just need to upload it again.\n'
2802 '(If you uploaded it with --no-squash, then branch dependencies '
2803 'are not supported, and you should reupload with --squash.)'
Christopher Lamf732cd52017-01-24 12:40:11 +11002804 % upstream_branch_name, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002805 else:
2806 parent = self.GetCommonAncestorWithUpstream()
2807
2808 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2809 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2810 '-m', message]).strip()
2811 else:
2812 change_desc = ChangeDescription(
2813 options.message or CreateDescriptionFromLog(args))
2814 if not change_desc.description:
2815 DieWithError("Description is empty. Aborting...")
2816
2817 if not git_footers.get_footer_change_id(change_desc.description):
2818 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002819 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2820 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002821 ref_to_push = 'HEAD'
2822 parent = '%s/%s' % (gerrit_remote, branch)
2823 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2824
2825 assert change_desc
2826 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2827 ref_to_push)]).splitlines()
2828 if len(commits) > 1:
2829 print('WARNING: This will upload %d commits. Run the following command '
2830 'to see which commits will be uploaded: ' % len(commits))
2831 print('git log %s..%s' % (parent, ref_to_push))
2832 print('You can also use `git squash-branch` to squash these into a '
2833 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002834 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002835
2836 if options.reviewers or options.tbr_owners:
2837 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2838 change)
2839
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002840 # Extra options that can be specified at push time. Doc:
2841 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2842 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002843 if change_desc.get_reviewers(tbr_only=True):
2844 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2845 refspec_opts.append('l=Code-Review+1')
2846
Aaron Gable9b713dd2016-12-14 16:04:21 -08002847 if title:
2848 if not re.match(r'^[\w ]+$', title):
2849 title = re.sub(r'[^\w ]', '', title)
Aaron Gable856585d2017-01-23 15:32:00 -08002850 if not automatic_title:
2851 print('WARNING: Patchset title may only contain alphanumeric chars '
Aaron Gableeb3af712017-02-02 12:42:02 -08002852 'and spaces. You can edit it in the UI. '
2853 'See https://crbug.com/663787.\n'
2854 'Cleaned up title: %s' % title)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002855 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2856 # reverse on its side.
Aaron Gable9b713dd2016-12-14 16:04:21 -08002857 refspec_opts.append('m=' + title.replace(' ', '_'))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002858
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002859 if options.send_mail:
2860 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002861 DieWithError('Must specify reviewers to send email.', change_desc)
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002862 refspec_opts.append('notify=ALL')
2863 else:
2864 refspec_opts.append('notify=NONE')
2865
tandrii99a72f22016-08-17 14:33:24 -07002866 reviewers = change_desc.get_reviewers()
2867 if reviewers:
Andrii Shyshkalovaf3a9992017-02-09 14:18:01 +01002868 # TODO(tandrii): remove this horrible hack once (Poly)Gerrit fixes their
2869 # side for real (b/34702620).
2870 def clean_invisible_chars(email):
2871 return email.decode('unicode_escape').encode('ascii', 'ignore')
2872 refspec_opts.extend('r=' + clean_invisible_chars(email).strip()
2873 for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002874
agablec6787972016-09-09 16:13:34 -07002875 if options.private:
2876 refspec_opts.append('draft')
2877
rmistry9eadede2016-09-19 11:22:43 -07002878 if options.topic:
2879 # Documentation on Gerrit topics is here:
2880 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2881 refspec_opts.append('topic=%s' % options.topic)
2882
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002883 refspec_suffix = ''
2884 if refspec_opts:
2885 refspec_suffix = '%' + ','.join(refspec_opts)
2886 assert ' ' not in refspec_suffix, (
2887 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002888 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002889
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002890 try:
2891 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalove9c78ff2017-02-06 15:53:13 +01002892 ['git', 'push', self.GetRemoteUrl(), refspec],
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002893 print_stdout=True,
2894 # Flush after every line: useful for seeing progress when running as
2895 # recipe.
2896 filter_fn=lambda _: sys.stdout.flush())
2897 except subprocess2.CalledProcessError:
2898 DieWithError('Failed to create a change. Please examine output above '
Christopher Lamf732cd52017-01-24 12:40:11 +11002899 'for the reason of the failure. ', change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002900
2901 if options.squash:
2902 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2903 change_numbers = [m.group(1)
2904 for m in map(regex.match, push_stdout.splitlines())
2905 if m]
2906 if len(change_numbers) != 1:
2907 DieWithError(
2908 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002909 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002910 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002911 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002912
2913 # Add cc's from the CC_LIST and --cc flag (if any).
2914 cc = self.GetCCList().split(',')
2915 if options.cc:
2916 cc.extend(options.cc)
2917 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002918 if change_desc.get_cced():
2919 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002920 if cc:
Aaron Gable5db82092016-11-17 10:52:08 -08002921 gerrit_util.AddReviewers(
Aaron Gable59f48512017-01-12 10:54:46 -08002922 self._GetGerritHost(), self.GetIssue(), cc,
2923 is_reviewer=False, notify=bool(options.send_mail))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002924 return 0
2925
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002926 def _AddChangeIdToCommitMessage(self, options, args):
2927 """Re-commits using the current message, assumes the commit hook is in
2928 place.
2929 """
2930 log_desc = options.message or CreateDescriptionFromLog(args)
2931 git_command = ['commit', '--amend', '-m', log_desc]
2932 RunGit(git_command)
2933 new_log_desc = CreateDescriptionFromLog(args)
2934 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002935 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002936 return new_log_desc
2937 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002938 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002939
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002940 def SetCQState(self, new_state):
2941 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002942 vote_map = {
2943 _CQState.NONE: 0,
2944 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002945 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002946 }
Andrii Shyshkalov828701b2016-12-09 10:46:47 +01002947 kwargs = {'labels': {'Commit-Queue': vote_map[new_state]}}
2948 if new_state == _CQState.DRY_RUN:
2949 # Don't spam everybody reviewer/owner.
2950 kwargs['notify'] = 'NONE'
2951 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(), **kwargs)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002952
tandriie113dfd2016-10-11 10:20:12 -07002953 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002954 try:
2955 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002956 except GerritChangeNotExists:
2957 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002958
2959 if data['status'] in ('ABANDONED', 'MERGED'):
2960 return 'CL %s is closed' % self.GetIssue()
2961
2962 def GetTryjobProperties(self, patchset=None):
2963 """Returns dictionary of properties to launch tryjob."""
2964 data = self._GetChangeDetail(['ALL_REVISIONS'])
2965 patchset = int(patchset or self.GetPatchset())
2966 assert patchset
2967 revision_data = None # Pylint wants it to be defined.
2968 for revision_data in data['revisions'].itervalues():
2969 if int(revision_data['_number']) == patchset:
2970 break
2971 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002972 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002973 (patchset, self.GetIssue()))
2974 return {
2975 'patch_issue': self.GetIssue(),
2976 'patch_set': patchset or self.GetPatchset(),
2977 'patch_project': data['project'],
2978 'patch_storage': 'gerrit',
2979 'patch_ref': revision_data['fetch']['http']['ref'],
2980 'patch_repository_url': revision_data['fetch']['http']['url'],
2981 'patch_gerrit_url': self.GetCodereviewServer(),
2982 }
tandriie113dfd2016-10-11 10:20:12 -07002983
tandriide281ae2016-10-12 06:02:30 -07002984 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002985 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002986
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002987
2988_CODEREVIEW_IMPLEMENTATIONS = {
2989 'rietveld': _RietveldChangelistImpl,
2990 'gerrit': _GerritChangelistImpl,
2991}
2992
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002993
iannuccie53c9352016-08-17 14:40:40 -07002994def _add_codereview_issue_select_options(parser, extra=""):
2995 _add_codereview_select_options(parser)
2996
2997 text = ('Operate on this issue number instead of the current branch\'s '
2998 'implicit issue.')
2999 if extra:
3000 text += ' '+extra
3001 parser.add_option('-i', '--issue', type=int, help=text)
3002
3003
3004def _process_codereview_issue_select_options(parser, options):
3005 _process_codereview_select_options(parser, options)
3006 if options.issue is not None and not options.forced_codereview:
3007 parser.error('--issue must be specified with either --rietveld or --gerrit')
3008
3009
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003010def _add_codereview_select_options(parser):
3011 """Appends --gerrit and --rietveld options to force specific codereview."""
3012 parser.codereview_group = optparse.OptionGroup(
3013 parser, 'EXPERIMENTAL! Codereview override options')
3014 parser.add_option_group(parser.codereview_group)
3015 parser.codereview_group.add_option(
3016 '--gerrit', action='store_true',
3017 help='Force the use of Gerrit for codereview')
3018 parser.codereview_group.add_option(
3019 '--rietveld', action='store_true',
3020 help='Force the use of Rietveld for codereview')
3021
3022
3023def _process_codereview_select_options(parser, options):
3024 if options.gerrit and options.rietveld:
3025 parser.error('Options --gerrit and --rietveld are mutually exclusive')
3026 options.forced_codereview = None
3027 if options.gerrit:
3028 options.forced_codereview = 'gerrit'
3029 elif options.rietveld:
3030 options.forced_codereview = 'rietveld'
3031
3032
tandriif9aefb72016-07-01 09:06:51 -07003033def _get_bug_line_values(default_project, bugs):
3034 """Given default_project and comma separated list of bugs, yields bug line
3035 values.
3036
3037 Each bug can be either:
3038 * a number, which is combined with default_project
3039 * string, which is left as is.
3040
3041 This function may produce more than one line, because bugdroid expects one
3042 project per line.
3043
3044 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3045 ['v8:123', 'chromium:789']
3046 """
3047 default_bugs = []
3048 others = []
3049 for bug in bugs.split(','):
3050 bug = bug.strip()
3051 if bug:
3052 try:
3053 default_bugs.append(int(bug))
3054 except ValueError:
3055 others.append(bug)
3056
3057 if default_bugs:
3058 default_bugs = ','.join(map(str, default_bugs))
3059 if default_project:
3060 yield '%s:%s' % (default_project, default_bugs)
3061 else:
3062 yield default_bugs
3063 for other in sorted(others):
3064 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3065 yield other
3066
3067
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003068class ChangeDescription(object):
3069 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003070 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003071 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Mark Mentovai600d3092017-03-08 12:58:18 -05003072 BUG_LINE = r'^[ \t]*(BUGS?|Bugs?)[ \t]*[:=][ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003073 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003074
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003075 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003076 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003077
agable@chromium.org42c20792013-09-12 17:34:49 +00003078 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003079 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003080 return '\n'.join(self._description_lines)
3081
3082 def set_description(self, desc):
3083 if isinstance(desc, basestring):
3084 lines = desc.splitlines()
3085 else:
3086 lines = [line.rstrip() for line in desc]
3087 while lines and not lines[0]:
3088 lines.pop(0)
3089 while lines and not lines[-1]:
3090 lines.pop(-1)
3091 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003092
piman@chromium.org336f9122014-09-04 02:16:55 +00003093 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003094 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003095 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00003096 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003097 return
agable@chromium.org42c20792013-09-12 17:34:49 +00003098 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003099
agable@chromium.org42c20792013-09-12 17:34:49 +00003100 # Get the set of R= and TBR= lines and remove them from the desciption.
3101 regexp = re.compile(self.R_LINE)
3102 matches = [regexp.match(line) for line in self._description_lines]
3103 new_desc = [l for i, l in enumerate(self._description_lines)
3104 if not matches[i]]
3105 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003106
agable@chromium.org42c20792013-09-12 17:34:49 +00003107 # Construct new unified R= and TBR= lines.
3108 r_names = []
3109 tbr_names = []
3110 for match in matches:
3111 if not match:
3112 continue
3113 people = cleanup_list([match.group(2).strip()])
3114 if match.group(1) == 'TBR':
3115 tbr_names.extend(people)
3116 else:
3117 r_names.extend(people)
3118 for name in r_names:
3119 if name not in reviewers:
3120 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003121 if add_owners_tbr:
3122 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003123 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003124 all_reviewers = set(tbr_names + reviewers)
3125 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3126 all_reviewers)
3127 tbr_names.extend(owners_db.reviewers_for(missing_files,
3128 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003129 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3130 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3131
3132 # Put the new lines in the description where the old first R= line was.
3133 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3134 if 0 <= line_loc < len(self._description_lines):
3135 if new_tbr_line:
3136 self._description_lines.insert(line_loc, new_tbr_line)
3137 if new_r_line:
3138 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003139 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003140 if new_r_line:
3141 self.append_footer(new_r_line)
3142 if new_tbr_line:
3143 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003144
tandriif9aefb72016-07-01 09:06:51 -07003145 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003146 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003147 self.set_description([
3148 '# Enter a description of the change.',
3149 '# This will be displayed on the codereview site.',
3150 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003151 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003152 '--------------------',
3153 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003154
agable@chromium.org42c20792013-09-12 17:34:49 +00003155 regexp = re.compile(self.BUG_LINE)
3156 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003157 prefix = settings.GetBugPrefix()
3158 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Mark Mentovai57c47212017-03-09 11:14:09 -05003159 bug_line_format = settings.GetBugLineFormat()
tandriif9aefb72016-07-01 09:06:51 -07003160 for value in values:
Mark Mentovai57c47212017-03-09 11:14:09 -05003161 self.append_footer(bug_line_format % value)
tandriif9aefb72016-07-01 09:06:51 -07003162
agable@chromium.org42c20792013-09-12 17:34:49 +00003163 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003164 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003165 if not content:
3166 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003167 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003168
3169 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003170 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3171 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003172 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003173 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003174
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003175 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003176 """Adds a footer line to the description.
3177
3178 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3179 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3180 that Gerrit footers are always at the end.
3181 """
3182 parsed_footer_line = git_footers.parse_footer(line)
3183 if parsed_footer_line:
3184 # Line is a gerrit footer in the form: Footer-Key: any value.
3185 # Thus, must be appended observing Gerrit footer rules.
3186 self.set_description(
3187 git_footers.add_footer(self.description,
3188 key=parsed_footer_line[0],
3189 value=parsed_footer_line[1]))
3190 return
3191
3192 if not self._description_lines:
3193 self._description_lines.append(line)
3194 return
3195
3196 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3197 if gerrit_footers:
3198 # git_footers.split_footers ensures that there is an empty line before
3199 # actual (gerrit) footers, if any. We have to keep it that way.
3200 assert top_lines and top_lines[-1] == ''
3201 top_lines, separator = top_lines[:-1], top_lines[-1:]
3202 else:
3203 separator = [] # No need for separator if there are no gerrit_footers.
3204
3205 prev_line = top_lines[-1] if top_lines else ''
3206 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3207 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3208 top_lines.append('')
3209 top_lines.append(line)
3210 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003211
tandrii99a72f22016-08-17 14:33:24 -07003212 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003213 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003214 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003215 reviewers = [match.group(2).strip()
3216 for match in matches
3217 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003218 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003219
bradnelsond975b302016-10-23 12:20:23 -07003220 def get_cced(self):
3221 """Retrieves the list of reviewers."""
3222 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3223 cced = [match.group(2).strip() for match in matches if match]
3224 return cleanup_list(cced)
3225
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003226 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3227 """Updates this commit description given the parent.
3228
3229 This is essentially what Gnumbd used to do.
3230 Consult https://goo.gl/WMmpDe for more details.
3231 """
3232 assert parent_msg # No, orphan branch creation isn't supported.
3233 assert parent_hash
3234 assert dest_ref
3235 parent_footer_map = git_footers.parse_footers(parent_msg)
3236 # This will also happily parse svn-position, which GnumbD is no longer
3237 # supporting. While we'd generate correct footers, the verifier plugin
3238 # installed in Gerrit will block such commit (ie git push below will fail).
3239 parent_position = git_footers.get_position(parent_footer_map)
3240
3241 # Cherry-picks may have last line obscuring their prior footers,
3242 # from git_footers perspective. This is also what Gnumbd did.
3243 cp_line = None
3244 if (self._description_lines and
3245 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3246 cp_line = self._description_lines.pop()
3247
3248 top_lines, _, parsed_footers = git_footers.split_footers(self.description)
3249
3250 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3251 # user interference with actual footers we'd insert below.
3252 for i, (k, v) in enumerate(parsed_footers):
3253 if k.startswith('Cr-'):
3254 parsed_footers[i] = (k.replace('Cr-', 'Cr-Original-'), v)
3255
3256 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003257 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003258 if parent_position[0] == dest_ref:
3259 # Same branch as parent.
3260 number = int(parent_position[1]) + 1
3261 else:
3262 number = 1 # New branch, and extra lineage.
3263 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3264 int(parent_position[1])))
3265
3266 parsed_footers.append(('Cr-Commit-Position',
3267 '%s@{#%d}' % (dest_ref, number)))
3268 parsed_footers.extend(('Cr-Branched-From', v) for v in lineage)
3269
3270 self._description_lines = top_lines
3271 if cp_line:
3272 self._description_lines.append(cp_line)
3273 if self._description_lines[-1] != '':
3274 self._description_lines.append('') # Ensure footer separator.
3275 self._description_lines.extend('%s: %s' % kv for kv in parsed_footers)
3276
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003277
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003278def get_approving_reviewers(props):
3279 """Retrieves the reviewers that approved a CL from the issue properties with
3280 messages.
3281
3282 Note that the list may contain reviewers that are not committer, thus are not
3283 considered by the CQ.
3284 """
3285 return sorted(
3286 set(
3287 message['sender']
3288 for message in props['messages']
3289 if message['approval'] and message['sender'] in props['reviewers']
3290 )
3291 )
3292
3293
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003294def FindCodereviewSettingsFile(filename='codereview.settings'):
3295 """Finds the given file starting in the cwd and going up.
3296
3297 Only looks up to the top of the repository unless an
3298 'inherit-review-settings-ok' file exists in the root of the repository.
3299 """
3300 inherit_ok_file = 'inherit-review-settings-ok'
3301 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003302 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003303 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3304 root = '/'
3305 while True:
3306 if filename in os.listdir(cwd):
3307 if os.path.isfile(os.path.join(cwd, filename)):
3308 return open(os.path.join(cwd, filename))
3309 if cwd == root:
3310 break
3311 cwd = os.path.dirname(cwd)
3312
3313
3314def LoadCodereviewSettingsFromFile(fileobj):
3315 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003316 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003317
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003318 def SetProperty(name, setting, unset_error_ok=False):
3319 fullname = 'rietveld.' + name
3320 if setting in keyvals:
3321 RunGit(['config', fullname, keyvals[setting]])
3322 else:
3323 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3324
tandrii48df5812016-10-17 03:55:37 -07003325 if not keyvals.get('GERRIT_HOST', False):
3326 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003327 # Only server setting is required. Other settings can be absent.
3328 # In that case, we ignore errors raised during option deletion attempt.
3329 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003330 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003331 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3332 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
Mark Mentovai57c47212017-03-09 11:14:09 -05003333 SetProperty('bug-line-format', 'BUG_LINE_FORMAT', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003334 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003335 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3336 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003337 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003338 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3339 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003340
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003341 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003342 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003343
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003344 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003345 RunGit(['config', 'gerrit.squash-uploads',
3346 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003347
tandrii@chromium.org28253532016-04-14 13:46:56 +00003348 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003349 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003350 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3351
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003352 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003353 # should be of the form
3354 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3355 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003356 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3357 keyvals['ORIGIN_URL_CONFIG']])
3358
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003359
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003360def urlretrieve(source, destination):
3361 """urllib is broken for SSL connections via a proxy therefore we
3362 can't use urllib.urlretrieve()."""
3363 with open(destination, 'w') as f:
3364 f.write(urllib2.urlopen(source).read())
3365
3366
ukai@chromium.org712d6102013-11-27 00:52:58 +00003367def hasSheBang(fname):
3368 """Checks fname is a #! script."""
3369 with open(fname) as f:
3370 return f.read(2).startswith('#!')
3371
3372
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003373# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3374def DownloadHooks(*args, **kwargs):
3375 pass
3376
3377
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003378def DownloadGerritHook(force):
3379 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003380
3381 Args:
3382 force: True to update hooks. False to install hooks if not present.
3383 """
3384 if not settings.GetIsGerrit():
3385 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003386 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003387 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3388 if not os.access(dst, os.X_OK):
3389 if os.path.exists(dst):
3390 if not force:
3391 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003392 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003393 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003394 if not hasSheBang(dst):
3395 DieWithError('Not a script: %s\n'
3396 'You need to download from\n%s\n'
3397 'into .git/hooks/commit-msg and '
3398 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003399 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3400 except Exception:
3401 if os.path.exists(dst):
3402 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003403 DieWithError('\nFailed to download hooks.\n'
3404 'You need to download from\n%s\n'
3405 'into .git/hooks/commit-msg and '
3406 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003407
3408
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003409def GetRietveldCodereviewSettingsInteractively():
3410 """Prompt the user for settings."""
3411 server = settings.GetDefaultServerUrl(error_ok=True)
3412 prompt = 'Rietveld server (host[:port])'
3413 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3414 newserver = ask_for_data(prompt + ':')
3415 if not server and not newserver:
3416 newserver = DEFAULT_SERVER
3417 if newserver:
3418 newserver = gclient_utils.UpgradeToHttps(newserver)
3419 if newserver != server:
3420 RunGit(['config', 'rietveld.server', newserver])
3421
3422 def SetProperty(initial, caption, name, is_url):
3423 prompt = caption
3424 if initial:
3425 prompt += ' ("x" to clear) [%s]' % initial
3426 new_val = ask_for_data(prompt + ':')
3427 if new_val == 'x':
3428 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3429 elif new_val:
3430 if is_url:
3431 new_val = gclient_utils.UpgradeToHttps(new_val)
3432 if new_val != initial:
3433 RunGit(['config', 'rietveld.' + name, new_val])
3434
3435 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3436 SetProperty(settings.GetDefaultPrivateFlag(),
3437 'Private flag (rietveld only)', 'private', False)
3438 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3439 'tree-status-url', False)
3440 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3441 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3442 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3443 'run-post-upload-hook', False)
3444
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003445
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003446def _ensure_default_gitcookies_path(configured_path, default_path):
3447 assert configured_path
3448 if configured_path == default_path:
3449 print('git is already configured to use your .gitcookies from %s' %
3450 configured_path)
3451 return
3452
3453 print('WARNING: you have configured custom path to .gitcookies: %s\n'
3454 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3455 (configured_path, default_path))
3456
3457 if not os.path.exists(configured_path):
3458 print('However, your configured .gitcookies file is missing.')
3459 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3460 action='reconfigure')
3461 RunGit(['config', '--global', 'http.cookiefile', default_path])
3462 return
3463
3464 if os.path.exists(default_path):
3465 print('WARNING: default .gitcookies file already exists %s' % default_path)
3466 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3467 default_path)
3468
3469 confirm_or_exit('Move existing .gitcookies to default location?',
3470 action='move')
3471 shutil.move(configured_path, default_path)
3472 RunGit(['config', '--global', 'http.cookiefile', default_path])
3473 print('Moved and reconfigured git to use .gitcookies from %s' % default_path)
3474
3475
3476def _configure_gitcookies_path(gitcookies_path):
3477 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3478 if os.path.exists(netrc_path):
3479 print('You seem to be using outdated .netrc for git credentials: %s' %
3480 netrc_path)
3481 print('This tool will guide you through setting up recommended '
3482 '.gitcookies store for git credentials.\n'
3483 '\n'
3484 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3485 ' git config --global --unset http.cookiefile\n'
3486 ' mv %s %s.backup\n\n' % (gitcookies_path, gitcookies_path))
3487 confirm_or_exit(action='setup .gitcookies')
3488 RunGit(['config', '--global', 'http.cookiefile', gitcookies_path])
3489 print('Configured git to use .gitcookies from %s' % gitcookies_path)
3490
3491
3492def CMDcreds_check(parser, args):
3493 """Checks credentials and suggests changes."""
3494 _, _ = parser.parse_args(args)
3495
3496 if gerrit_util.GceAuthenticator.is_gce():
3497 DieWithError('this command is not designed for GCE, are you on a bot?')
3498
3499 gitcookies_path = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3500 configured_gitcookies_path = RunGitSilent(
3501 ['config', '--global', 'http.cookiefile']).strip()
3502 if configured_gitcookies_path:
3503 _ensure_default_gitcookies_path(configured_gitcookies_path, gitcookies_path)
3504 else:
3505 _configure_gitcookies_path(gitcookies_path)
3506 # TODO(tandrii): finish this.
3507 return 0
3508
3509
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003510@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003511def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003512 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003513
tandrii5d0a0422016-09-14 06:24:35 -07003514 print('WARNING: git cl config works for Rietveld only')
3515 # TODO(tandrii): remove this once we switch to Gerrit.
3516 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003517 parser.add_option('--activate-update', action='store_true',
3518 help='activate auto-updating [rietveld] section in '
3519 '.git/config')
3520 parser.add_option('--deactivate-update', action='store_true',
3521 help='deactivate auto-updating [rietveld] section in '
3522 '.git/config')
3523 options, args = parser.parse_args(args)
3524
3525 if options.deactivate_update:
3526 RunGit(['config', 'rietveld.autoupdate', 'false'])
3527 return
3528
3529 if options.activate_update:
3530 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3531 return
3532
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003533 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003534 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003535 return 0
3536
3537 url = args[0]
3538 if not url.endswith('codereview.settings'):
3539 url = os.path.join(url, 'codereview.settings')
3540
3541 # Load code review settings and download hooks (if available).
3542 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3543 return 0
3544
3545
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003546def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003547 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003548 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3549 branch = ShortBranchName(branchref)
3550 _, args = parser.parse_args(args)
3551 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003552 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003553 return RunGit(['config', 'branch.%s.base-url' % branch],
3554 error_ok=False).strip()
3555 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003556 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003557 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3558 error_ok=False).strip()
3559
3560
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003561def color_for_status(status):
3562 """Maps a Changelist status to color, for CMDstatus and other tools."""
3563 return {
3564 'unsent': Fore.RED,
3565 'waiting': Fore.BLUE,
3566 'reply': Fore.YELLOW,
3567 'lgtm': Fore.GREEN,
3568 'commit': Fore.MAGENTA,
3569 'closed': Fore.CYAN,
3570 'error': Fore.WHITE,
3571 }.get(status, Fore.WHITE)
3572
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003573
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003574def get_cl_statuses(changes, fine_grained, max_processes=None):
3575 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003576
3577 If fine_grained is true, this will fetch CL statuses from the server.
3578 Otherwise, simply indicate if there's a matching url for the given branches.
3579
3580 If max_processes is specified, it is used as the maximum number of processes
3581 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3582 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003583
3584 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003585 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003586 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003587 upload.verbosity = 0
3588
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003589 if not changes:
3590 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003591
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003592 if not fine_grained:
3593 # Fast path which doesn't involve querying codereview servers.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003594 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003595 for cl in changes:
3596 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003597 return
3598
3599 # First, sort out authentication issues.
3600 logging.debug('ensuring credentials exist')
3601 for cl in changes:
3602 cl.EnsureAuthenticated(force=False, refresh=True)
3603
3604 def fetch(cl):
3605 try:
3606 return (cl, cl.GetStatus())
3607 except:
3608 # See http://crbug.com/629863.
3609 logging.exception('failed to fetch status for %s:', cl)
3610 raise
3611
3612 threads_count = len(changes)
3613 if max_processes:
3614 threads_count = max(1, min(threads_count, max_processes))
3615 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3616
3617 pool = ThreadPool(threads_count)
3618 fetched_cls = set()
3619 try:
3620 it = pool.imap_unordered(fetch, changes).__iter__()
3621 while True:
3622 try:
3623 cl, status = it.next(timeout=5)
3624 except multiprocessing.TimeoutError:
3625 break
3626 fetched_cls.add(cl)
3627 yield cl, status
3628 finally:
3629 pool.close()
3630
3631 # Add any branches that failed to fetch.
3632 for cl in set(changes) - fetched_cls:
3633 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003634
rmistry@google.com2dd99862015-06-22 12:22:18 +00003635
3636def upload_branch_deps(cl, args):
3637 """Uploads CLs of local branches that are dependents of the current branch.
3638
3639 If the local branch dependency tree looks like:
3640 test1 -> test2.1 -> test3.1
3641 -> test3.2
3642 -> test2.2 -> test3.3
3643
3644 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3645 run on the dependent branches in this order:
3646 test2.1, test3.1, test3.2, test2.2, test3.3
3647
3648 Note: This function does not rebase your local dependent branches. Use it when
3649 you make a change to the parent branch that will not conflict with its
3650 dependent branches, and you would like their dependencies updated in
3651 Rietveld.
3652 """
3653 if git_common.is_dirty_git_tree('upload-branch-deps'):
3654 return 1
3655
3656 root_branch = cl.GetBranch()
3657 if root_branch is None:
3658 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3659 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01003660 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00003661 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3662 'patchset dependencies without an uploaded CL.')
3663
3664 branches = RunGit(['for-each-ref',
3665 '--format=%(refname:short) %(upstream:short)',
3666 'refs/heads'])
3667 if not branches:
3668 print('No local branches found.')
3669 return 0
3670
3671 # Create a dictionary of all local branches to the branches that are dependent
3672 # on it.
3673 tracked_to_dependents = collections.defaultdict(list)
3674 for b in branches.splitlines():
3675 tokens = b.split()
3676 if len(tokens) == 2:
3677 branch_name, tracked = tokens
3678 tracked_to_dependents[tracked].append(branch_name)
3679
vapiera7fbd5a2016-06-16 09:17:49 -07003680 print()
3681 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003682 dependents = []
3683 def traverse_dependents_preorder(branch, padding=''):
3684 dependents_to_process = tracked_to_dependents.get(branch, [])
3685 padding += ' '
3686 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003687 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003688 dependents.append(dependent)
3689 traverse_dependents_preorder(dependent, padding)
3690 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003691 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003692
3693 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003694 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003695 return 0
3696
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003697 confirm_or_exit('This command will checkout all dependent branches and run '
3698 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003699
andybons@chromium.org962f9462016-02-03 20:00:42 +00003700 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003701 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003702 args.extend(['-t', 'Updated patchset dependency'])
3703
rmistry@google.com2dd99862015-06-22 12:22:18 +00003704 # Record all dependents that failed to upload.
3705 failures = {}
3706 # Go through all dependents, checkout the branch and upload.
3707 try:
3708 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003709 print()
3710 print('--------------------------------------')
3711 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003712 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003713 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003714 try:
3715 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003716 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003717 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003718 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003719 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003720 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003721 finally:
3722 # Swap back to the original root branch.
3723 RunGit(['checkout', '-q', root_branch])
3724
vapiera7fbd5a2016-06-16 09:17:49 -07003725 print()
3726 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003727 for dependent_branch in dependents:
3728 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003729 print(' %s : %s' % (dependent_branch, upload_status))
3730 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003731
3732 return 0
3733
3734
kmarshall3bff56b2016-06-06 18:31:47 -07003735def CMDarchive(parser, args):
3736 """Archives and deletes branches associated with closed changelists."""
3737 parser.add_option(
3738 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003739 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003740 parser.add_option(
3741 '-f', '--force', action='store_true',
3742 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003743 parser.add_option(
3744 '-d', '--dry-run', action='store_true',
3745 help='Skip the branch tagging and removal steps.')
3746 parser.add_option(
3747 '-t', '--notags', action='store_true',
3748 help='Do not tag archived branches. '
3749 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003750
3751 auth.add_auth_options(parser)
3752 options, args = parser.parse_args(args)
3753 if args:
3754 parser.error('Unsupported args: %s' % ' '.join(args))
3755 auth_config = auth.extract_auth_config_from_options(options)
3756
3757 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3758 if not branches:
3759 return 0
3760
vapiera7fbd5a2016-06-16 09:17:49 -07003761 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003762 changes = [Changelist(branchref=b, auth_config=auth_config)
3763 for b in branches.splitlines()]
3764 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3765 statuses = get_cl_statuses(changes,
3766 fine_grained=True,
3767 max_processes=options.maxjobs)
3768 proposal = [(cl.GetBranch(),
3769 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3770 for cl, status in statuses
3771 if status == 'closed']
3772 proposal.sort()
3773
3774 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003775 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003776 return 0
3777
3778 current_branch = GetCurrentBranch()
3779
vapiera7fbd5a2016-06-16 09:17:49 -07003780 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003781 if options.notags:
3782 for next_item in proposal:
3783 print(' ' + next_item[0])
3784 else:
3785 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3786 for next_item in proposal:
3787 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003788
kmarshall9249e012016-08-23 12:02:16 -07003789 # Quit now on precondition failure or if instructed by the user, either
3790 # via an interactive prompt or by command line flags.
3791 if options.dry_run:
3792 print('\nNo changes were made (dry run).\n')
3793 return 0
3794 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003795 print('You are currently on a branch \'%s\' which is associated with a '
3796 'closed codereview issue, so archive cannot proceed. Please '
3797 'checkout another branch and run this command again.' %
3798 current_branch)
3799 return 1
kmarshall9249e012016-08-23 12:02:16 -07003800 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003801 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3802 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003803 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003804 return 1
3805
3806 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003807 if not options.notags:
3808 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003809 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003810
vapiera7fbd5a2016-06-16 09:17:49 -07003811 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003812
3813 return 0
3814
3815
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003816def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003817 """Show status of changelists.
3818
3819 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003820 - Red not sent for review or broken
3821 - Blue waiting for review
3822 - Yellow waiting for you to reply to review
3823 - Green LGTM'ed
3824 - Magenta in the commit queue
3825 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003826
3827 Also see 'git cl comments'.
3828 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003829 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003830 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003831 parser.add_option('-f', '--fast', action='store_true',
3832 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003833 parser.add_option(
3834 '-j', '--maxjobs', action='store', type=int,
3835 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003836
3837 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003838 _add_codereview_issue_select_options(
3839 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003840 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003841 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003842 if args:
3843 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003844 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003845
iannuccie53c9352016-08-17 14:40:40 -07003846 if options.issue is not None and not options.field:
3847 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003848
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003849 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003850 cl = Changelist(auth_config=auth_config, issue=options.issue,
3851 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003852 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003853 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003854 elif options.field == 'id':
3855 issueid = cl.GetIssue()
3856 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003857 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003858 elif options.field == 'patch':
3859 patchset = cl.GetPatchset()
3860 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003861 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003862 elif options.field == 'status':
3863 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003864 elif options.field == 'url':
3865 url = cl.GetIssueURL()
3866 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003867 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003868 return 0
3869
3870 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3871 if not branches:
3872 print('No local branch found.')
3873 return 0
3874
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003875 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003876 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003877 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003878 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003879 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003880 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003881 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003882
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003883 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003884 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3885 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3886 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003887 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003888 c, status = output.next()
3889 branch_statuses[c.GetBranch()] = status
3890 status = branch_statuses.pop(branch)
3891 url = cl.GetIssueURL()
3892 if url and (not status or status == 'error'):
3893 # The issue probably doesn't exist anymore.
3894 url += ' (broken)'
3895
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003896 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003897 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003898 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003899 color = ''
3900 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003901 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003902 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003903 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003904 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003905
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003906
3907 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07003908 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003909 print('Current branch: %s' % branch)
3910 for cl in changes:
3911 if cl.GetBranch() == branch:
3912 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003913 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003914 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003915 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003916 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003917 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003918 print('Issue description:')
3919 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003920 return 0
3921
3922
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003923def colorize_CMDstatus_doc():
3924 """To be called once in main() to add colors to git cl status help."""
3925 colors = [i for i in dir(Fore) if i[0].isupper()]
3926
3927 def colorize_line(line):
3928 for color in colors:
3929 if color in line.upper():
3930 # Extract whitespaces first and the leading '-'.
3931 indent = len(line) - len(line.lstrip(' ')) + 1
3932 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3933 return line
3934
3935 lines = CMDstatus.__doc__.splitlines()
3936 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3937
3938
phajdan.jre328cf92016-08-22 04:12:17 -07003939def write_json(path, contents):
3940 with open(path, 'w') as f:
3941 json.dump(contents, f)
3942
3943
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003944@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003945def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003946 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003947
3948 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003949 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003950 parser.add_option('-r', '--reverse', action='store_true',
3951 help='Lookup the branch(es) for the specified issues. If '
3952 'no issues are specified, all branches with mapped '
3953 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003954 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003955 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003956 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003957 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003958
dnj@chromium.org406c4402015-03-03 17:22:28 +00003959 if options.reverse:
3960 branches = RunGit(['for-each-ref', 'refs/heads',
3961 '--format=%(refname:short)']).splitlines()
3962
3963 # Reverse issue lookup.
3964 issue_branch_map = {}
3965 for branch in branches:
3966 cl = Changelist(branchref=branch)
3967 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3968 if not args:
3969 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003970 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003971 for issue in args:
3972 if not issue:
3973 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003974 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003975 print('Branch for issue number %s: %s' % (
3976 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003977 if options.json:
3978 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003979 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003980 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003981 if len(args) > 0:
3982 try:
3983 issue = int(args[0])
3984 except ValueError:
3985 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003986 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003987 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003988 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003989 if options.json:
3990 write_json(options.json, {
3991 'issue': cl.GetIssue(),
3992 'issue_url': cl.GetIssueURL(),
3993 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003994 return 0
3995
3996
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003997def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003998 """Shows or posts review comments for any changelist."""
3999 parser.add_option('-a', '--add-comment', dest='comment',
4000 help='comment to add to an issue')
4001 parser.add_option('-i', dest='issue',
4002 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00004003 parser.add_option('-j', '--json-file',
4004 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004005 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004006 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004007 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004008
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004009 issue = None
4010 if options.issue:
4011 try:
4012 issue = int(options.issue)
4013 except ValueError:
4014 DieWithError('A review issue id is expected to be a number')
4015
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00004016 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004017
4018 if options.comment:
4019 cl.AddComment(options.comment)
4020 return 0
4021
4022 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00004023 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00004024 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00004025 summary.append({
4026 'date': message['date'],
4027 'lgtm': False,
4028 'message': message['text'],
4029 'not_lgtm': False,
4030 'sender': message['sender'],
4031 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004032 if message['disapproval']:
4033 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00004034 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004035 elif message['approval']:
4036 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00004037 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004038 elif message['sender'] == data['owner_email']:
4039 color = Fore.MAGENTA
4040 else:
4041 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07004042 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004043 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07004044 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004045 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004046 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00004047 if options.json_file:
4048 with open(options.json_file, 'wb') as f:
4049 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004050 return 0
4051
4052
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004053@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004054def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004055 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004056 parser.add_option('-d', '--display', action='store_true',
4057 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004058 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004059 help='New description to set for this issue (- for stdin, '
4060 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004061 parser.add_option('-f', '--force', action='store_true',
4062 help='Delete any unpublished Gerrit edits for this issue '
4063 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004064
4065 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004066 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004067 options, args = parser.parse_args(args)
4068 _process_codereview_select_options(parser, options)
4069
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004070 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004071 if len(args) > 0:
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004072 target_issue_arg = ParseIssueNumberArgument(args[0])
4073 if not target_issue_arg.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004074 parser.print_help()
4075 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004076
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004077 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004078
martiniss6eda05f2016-06-30 10:18:35 -07004079 kwargs = {
4080 'auth_config': auth_config,
4081 'codereview': options.forced_codereview,
4082 }
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004083 if target_issue_arg:
4084 kwargs['issue'] = target_issue_arg.issue
4085 kwargs['codereview_host'] = target_issue_arg.hostname
martiniss6eda05f2016-06-30 10:18:35 -07004086
4087 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004088
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004089 if not cl.GetIssue():
4090 DieWithError('This branch has no associated changelist.')
4091 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004092
smut@google.com34fb6b12015-07-13 20:03:26 +00004093 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004094 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004095 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004096
4097 if options.new_description:
4098 text = options.new_description
4099 if text == '-':
4100 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004101 elif text == '+':
4102 base_branch = cl.GetCommonAncestorWithUpstream()
4103 change = cl.GetChange(base_branch, None, local_description=True)
4104 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004105
4106 description.set_description(text)
4107 else:
4108 description.prompt()
4109
wychen@chromium.org063e4e52015-04-03 06:51:44 +00004110 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004111 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004112 return 0
4113
4114
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004115def CreateDescriptionFromLog(args):
4116 """Pulls out the commit log to use as a base for the CL description."""
4117 log_args = []
4118 if len(args) == 1 and not args[0].endswith('.'):
4119 log_args = [args[0] + '..']
4120 elif len(args) == 1 and args[0].endswith('...'):
4121 log_args = [args[0][:-1]]
4122 elif len(args) == 2:
4123 log_args = [args[0] + '..' + args[1]]
4124 else:
4125 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004126 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004127
4128
thestig@chromium.org44202a22014-03-11 19:22:18 +00004129def CMDlint(parser, args):
4130 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004131 parser.add_option('--filter', action='append', metavar='-x,+y',
4132 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004133 auth.add_auth_options(parser)
4134 options, args = parser.parse_args(args)
4135 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004136
4137 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004138 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004139 try:
4140 import cpplint
4141 import cpplint_chromium
4142 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004143 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004144 return 1
4145
4146 # Change the current working directory before calling lint so that it
4147 # shows the correct base.
4148 previous_cwd = os.getcwd()
4149 os.chdir(settings.GetRoot())
4150 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004151 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004152 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4153 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004154 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004155 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004156 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004157
4158 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004159 command = args + files
4160 if options.filter:
4161 command = ['--filter=' + ','.join(options.filter)] + command
4162 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004163
4164 white_regex = re.compile(settings.GetLintRegex())
4165 black_regex = re.compile(settings.GetLintIgnoreRegex())
4166 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4167 for filename in filenames:
4168 if white_regex.match(filename):
4169 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004170 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004171 else:
4172 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4173 extra_check_functions)
4174 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004175 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004176 finally:
4177 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004178 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004179 if cpplint._cpplint_state.error_count != 0:
4180 return 1
4181 return 0
4182
4183
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004184def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004185 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004186 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004187 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004188 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004189 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004190 auth.add_auth_options(parser)
4191 options, args = parser.parse_args(args)
4192 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004193
sbc@chromium.org71437c02015-04-09 19:29:40 +00004194 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004195 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004196 return 1
4197
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004198 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004199 if args:
4200 base_branch = args[0]
4201 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004202 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004203 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004204
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004205 cl.RunHook(
4206 committing=not options.upload,
4207 may_prompt=False,
4208 verbose=options.verbose,
4209 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004210 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004211
4212
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004213def GenerateGerritChangeId(message):
4214 """Returns Ixxxxxx...xxx change id.
4215
4216 Works the same way as
4217 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4218 but can be called on demand on all platforms.
4219
4220 The basic idea is to generate git hash of a state of the tree, original commit
4221 message, author/committer info and timestamps.
4222 """
4223 lines = []
4224 tree_hash = RunGitSilent(['write-tree'])
4225 lines.append('tree %s' % tree_hash.strip())
4226 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4227 if code == 0:
4228 lines.append('parent %s' % parent.strip())
4229 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4230 lines.append('author %s' % author.strip())
4231 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4232 lines.append('committer %s' % committer.strip())
4233 lines.append('')
4234 # Note: Gerrit's commit-hook actually cleans message of some lines and
4235 # whitespace. This code is not doing this, but it clearly won't decrease
4236 # entropy.
4237 lines.append(message)
4238 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4239 stdin='\n'.join(lines))
4240 return 'I%s' % change_hash.strip()
4241
4242
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004243def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004244 """Computes the remote branch ref to use for the CL.
4245
4246 Args:
4247 remote (str): The git remote for the CL.
4248 remote_branch (str): The git remote branch for the CL.
4249 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004250 """
4251 if not (remote and remote_branch):
4252 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004253
wittman@chromium.org455dc922015-01-26 20:15:50 +00004254 if target_branch:
4255 # Cannonicalize branch references to the equivalent local full symbolic
4256 # refs, which are then translated into the remote full symbolic refs
4257 # below.
4258 if '/' not in target_branch:
4259 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4260 else:
4261 prefix_replacements = (
4262 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4263 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4264 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4265 )
4266 match = None
4267 for regex, replacement in prefix_replacements:
4268 match = re.search(regex, target_branch)
4269 if match:
4270 remote_branch = target_branch.replace(match.group(0), replacement)
4271 break
4272 if not match:
4273 # This is a branch path but not one we recognize; use as-is.
4274 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004275 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4276 # Handle the refs that need to land in different refs.
4277 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004278
wittman@chromium.org455dc922015-01-26 20:15:50 +00004279 # Create the true path to the remote branch.
4280 # Does the following translation:
4281 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4282 # * refs/remotes/origin/master -> refs/heads/master
4283 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4284 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4285 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4286 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4287 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4288 'refs/heads/')
4289 elif remote_branch.startswith('refs/remotes/branch-heads'):
4290 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004291
wittman@chromium.org455dc922015-01-26 20:15:50 +00004292 return remote_branch
4293
4294
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004295def cleanup_list(l):
4296 """Fixes a list so that comma separated items are put as individual items.
4297
4298 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4299 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4300 """
4301 items = sum((i.split(',') for i in l), [])
4302 stripped_items = (i.strip() for i in items)
4303 return sorted(filter(None, stripped_items))
4304
4305
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004306@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004307def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004308 """Uploads the current changelist to codereview.
4309
4310 Can skip dependency patchset uploads for a branch by running:
4311 git config branch.branch_name.skip-deps-uploads True
4312 To unset run:
4313 git config --unset branch.branch_name.skip-deps-uploads
4314 Can also set the above globally by using the --global flag.
4315 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004316 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4317 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004318 parser.add_option('--bypass-watchlists', action='store_true',
4319 dest='bypass_watchlists',
4320 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004321 parser.add_option('-f', action='store_true', dest='force',
4322 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004323 parser.add_option('--message', '-m', dest='message',
4324 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004325 parser.add_option('-b', '--bug',
4326 help='pre-populate the bug number(s) for this issue. '
4327 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004328 parser.add_option('--message-file', dest='message_file',
4329 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004330 parser.add_option('--title', '-t', dest='title',
4331 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004332 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004333 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004334 help='reviewer email addresses')
4335 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004336 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004337 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004338 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004339 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004340 parser.add_option('--emulate_svn_auto_props',
4341 '--emulate-svn-auto-props',
4342 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004343 dest="emulate_svn_auto_props",
4344 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004345 parser.add_option('-c', '--use-commit-queue', action='store_true',
4346 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004347 parser.add_option('--private', action='store_true',
4348 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004349 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004350 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004351 metavar='TARGET',
4352 help='Apply CL to remote ref TARGET. ' +
4353 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004354 parser.add_option('--squash', action='store_true',
4355 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004356 parser.add_option('--no-squash', action='store_true',
4357 help='Don\'t squash multiple commits into one ' +
4358 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004359 parser.add_option('--topic', default=None,
4360 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004361 parser.add_option('--email', default=None,
4362 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004363 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4364 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004365 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4366 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004367 help='Send the patchset to do a CQ dry run right after '
4368 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004369 parser.add_option('--dependencies', action='store_true',
4370 help='Uploads CLs of all the local branches that depend on '
4371 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004372
rmistry@google.com2dd99862015-06-22 12:22:18 +00004373 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004374 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004375 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004376 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004377 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004378 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004379 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004380
sbc@chromium.org71437c02015-04-09 19:29:40 +00004381 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004382 return 1
4383
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004384 options.reviewers = cleanup_list(options.reviewers)
4385 options.cc = cleanup_list(options.cc)
4386
tandriib80458a2016-06-23 12:20:07 -07004387 if options.message_file:
4388 if options.message:
4389 parser.error('only one of --message and --message-file allowed.')
4390 options.message = gclient_utils.FileRead(options.message_file)
4391 options.message_file = None
4392
tandrii4d0545a2016-07-06 03:56:49 -07004393 if options.cq_dry_run and options.use_commit_queue:
4394 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4395
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004396 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4397 settings.GetIsGerrit()
4398
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004399 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004400 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004401
4402
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004403@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004404def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004405 """DEPRECATED: Used to commit the current changelist via git-svn."""
4406 message = ('git-cl no longer supports committing to SVN repositories via '
4407 'git-svn. You probably want to use `git cl land` instead.')
4408 print(message)
4409 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004410
4411
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004412@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004413def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004414 """Commits the current changelist via git.
4415
4416 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4417 upstream and closes the issue automatically and atomically.
4418
4419 Otherwise (in case of Rietveld):
4420 Squashes branch into a single commit.
4421 Updates commit message with metadata (e.g. pointer to review).
4422 Pushes the code upstream.
4423 Updates review and closes.
4424 """
4425 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4426 help='bypass upload presubmit hook')
4427 parser.add_option('-m', dest='message',
4428 help="override review description")
4429 parser.add_option('-f', action='store_true', dest='force',
4430 help="force yes to questions (don't prompt)")
4431 parser.add_option('-c', dest='contributor',
4432 help="external contributor for patch (appended to " +
4433 "description and used as author for git). Should be " +
4434 "formatted as 'First Last <email@example.com>'")
4435 add_git_similarity(parser)
4436 auth.add_auth_options(parser)
4437 (options, args) = parser.parse_args(args)
4438 auth_config = auth.extract_auth_config_from_options(options)
4439
4440 cl = Changelist(auth_config=auth_config)
4441
4442 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4443 if cl.IsGerrit():
4444 if options.message:
4445 # This could be implemented, but it requires sending a new patch to
4446 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4447 # Besides, Gerrit has the ability to change the commit message on submit
4448 # automatically, thus there is no need to support this option (so far?).
4449 parser.error('-m MESSAGE option is not supported for Gerrit.')
4450 if options.contributor:
4451 parser.error(
4452 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4453 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4454 'the contributor\'s "name <email>". If you can\'t upload such a '
4455 'commit for review, contact your repository admin and request'
4456 '"Forge-Author" permission.')
4457 if not cl.GetIssue():
4458 DieWithError('You must upload the change first to Gerrit.\n'
4459 ' If you would rather have `git cl land` upload '
4460 'automatically for you, see http://crbug.com/642759')
4461 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4462 options.verbose)
4463
4464 current = cl.GetBranch()
4465 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4466 if remote == '.':
4467 print()
4468 print('Attempting to push branch %r into another local branch!' % current)
4469 print()
4470 print('Either reparent this branch on top of origin/master:')
4471 print(' git reparent-branch --root')
4472 print()
4473 print('OR run `git rebase-update` if you think the parent branch is ')
4474 print('already committed.')
4475 print()
4476 print(' Current parent: %r' % upstream_branch)
4477 return 1
4478
4479 if not args:
4480 # Default to merging against our best guess of the upstream branch.
4481 args = [cl.GetUpstreamBranch()]
4482
4483 if options.contributor:
4484 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
4485 print("Please provide contibutor as 'First Last <email@example.com>'")
4486 return 1
4487
4488 base_branch = args[0]
4489
4490 if git_common.is_dirty_git_tree('land'):
4491 return 1
4492
4493 # This rev-list syntax means "show all commits not in my branch that
4494 # are in base_branch".
4495 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4496 base_branch]).splitlines()
4497 if upstream_commits:
4498 print('Base branch "%s" has %d commits '
4499 'not in this branch.' % (base_branch, len(upstream_commits)))
4500 print('Run "git merge %s" before attempting to land.' % base_branch)
4501 return 1
4502
4503 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
4504 if not options.bypass_hooks:
4505 author = None
4506 if options.contributor:
4507 author = re.search(r'\<(.*)\>', options.contributor).group(1)
4508 hook_results = cl.RunHook(
4509 committing=True,
4510 may_prompt=not options.force,
4511 verbose=options.verbose,
4512 change=cl.GetChange(merge_base, author))
4513 if not hook_results.should_continue():
4514 return 1
4515
4516 # Check the tree status if the tree status URL is set.
4517 status = GetTreeStatus()
4518 if 'closed' == status:
4519 print('The tree is closed. Please wait for it to reopen. Use '
4520 '"git cl land --bypass-hooks" to commit on a closed tree.')
4521 return 1
4522 elif 'unknown' == status:
4523 print('Unable to determine tree status. Please verify manually and '
4524 'use "git cl land --bypass-hooks" to commit on a closed tree.')
4525 return 1
4526
4527 change_desc = ChangeDescription(options.message)
4528 if not change_desc.description and cl.GetIssue():
4529 change_desc = ChangeDescription(cl.GetDescription())
4530
4531 if not change_desc.description:
4532 if not cl.GetIssue() and options.bypass_hooks:
4533 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
4534 else:
4535 print('No description set.')
4536 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
4537 return 1
4538
4539 # Keep a separate copy for the commit message, because the commit message
4540 # contains the link to the Rietveld issue, while the Rietveld message contains
4541 # the commit viewvc url.
4542 if cl.GetIssue():
4543 change_desc.update_reviewers(cl.GetApprovingReviewers())
4544
4545 commit_desc = ChangeDescription(change_desc.description)
4546 if cl.GetIssue():
4547 # Xcode won't linkify this URL unless there is a non-whitespace character
4548 # after it. Add a period on a new line to circumvent this. Also add a space
4549 # before the period to make sure that Gitiles continues to correctly resolve
4550 # the URL.
4551 commit_desc.append_footer('Review-Url: %s .' % cl.GetIssueURL())
4552 if options.contributor:
4553 commit_desc.append_footer('Patch from %s.' % options.contributor)
4554
4555 print('Description:')
4556 print(commit_desc.description)
4557
4558 branches = [merge_base, cl.GetBranchRef()]
4559 if not options.force:
4560 print_stats(options.similarity, options.find_copies, branches)
4561
4562 # We want to squash all this branch's commits into one commit with the proper
4563 # description. We do this by doing a "reset --soft" to the base branch (which
4564 # keeps the working copy the same), then landing that.
4565 MERGE_BRANCH = 'git-cl-commit'
4566 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4567 # Delete the branches if they exist.
4568 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4569 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4570 result = RunGitWithCode(showref_cmd)
4571 if result[0] == 0:
4572 RunGit(['branch', '-D', branch])
4573
4574 # We might be in a directory that's present in this branch but not in the
4575 # trunk. Move up to the top of the tree so that git commands that expect a
4576 # valid CWD won't fail after we check out the merge branch.
4577 rel_base_path = settings.GetRelativeRoot()
4578 if rel_base_path:
4579 os.chdir(rel_base_path)
4580
4581 # Stuff our change into the merge branch.
4582 # We wrap in a try...finally block so if anything goes wrong,
4583 # we clean up the branches.
4584 retcode = -1
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004585 revision = None
4586 try:
4587 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
4588 RunGit(['reset', '--soft', merge_base])
4589 if options.contributor:
4590 RunGit(
4591 [
4592 'commit', '--author', options.contributor,
4593 '-m', commit_desc.description,
4594 ])
4595 else:
4596 RunGit(['commit', '-m', commit_desc.description])
4597
4598 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
4599 mirror = settings.GetGitMirror(remote)
4600 if mirror:
4601 pushurl = mirror.url
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004602 git_numberer_enabled = _is_git_numberer_enabled(pushurl, branch)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004603 else:
4604 pushurl = remote # Usually, this is 'origin'.
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004605 git_numberer_enabled = _is_git_numberer_enabled(
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004606 RunGit(['config', 'remote.%s.url' % remote]).strip(), branch)
4607
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004608 if git_numberer_enabled:
4609 # TODO(tandrii): maybe do autorebase + retry on failure
4610 # http://crbug.com/682934, but better just use Gerrit :)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004611 logging.debug('Adding git number footers')
4612 parent_msg = RunGit(['show', '-s', '--format=%B', merge_base]).strip()
4613 commit_desc.update_with_git_number_footers(merge_base, parent_msg,
4614 branch)
4615 # Ensure timestamps are monotonically increasing.
4616 timestamp = max(1 + _get_committer_timestamp(merge_base),
4617 _get_committer_timestamp('HEAD'))
4618 _git_amend_head(commit_desc.description, timestamp)
4619 change_desc = ChangeDescription(commit_desc.description)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004620
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004621 retcode, output = RunGitWithCode(
4622 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004623 if retcode == 0:
4624 revision = RunGit(['rev-parse', 'HEAD']).strip()
4625 logging.debug(output)
4626 except: # pylint: disable=bare-except
4627 if _IS_BEING_TESTED:
4628 logging.exception('this is likely your ACTUAL cause of test failure.\n'
4629 + '-' * 30 + '8<' + '-' * 30)
4630 logging.error('\n' + '-' * 30 + '8<' + '-' * 30 + '\n\n\n')
4631 raise
4632 finally:
4633 # And then swap back to the original branch and clean up.
4634 RunGit(['checkout', '-q', cl.GetBranch()])
4635 RunGit(['branch', '-D', MERGE_BRANCH])
4636
4637 if not revision:
4638 print('Failed to push. If this persists, please file a bug.')
4639 return 1
4640
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004641 if cl.GetIssue():
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004642 viewvc_url = settings.GetViewVCUrl()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004643 if viewvc_url and revision:
4644 change_desc.append_footer(
4645 'Committed: %s%s' % (viewvc_url, revision))
4646 elif revision:
4647 change_desc.append_footer('Committed: %s' % (revision,))
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004648 print('Closing issue '
4649 '(you may be prompted for your codereview password)...')
4650 cl.UpdateDescription(change_desc.description)
4651 cl.CloseIssue()
4652 props = cl.GetIssueProperties()
4653 patch_num = len(props['patchsets'])
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004654 comment = "Committed patchset #%d (id:%d) manually as %s" % (
4655 patch_num, props['patchsets'][-1], revision)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004656 if options.bypass_hooks:
4657 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4658 else:
4659 comment += ' (presubmit successful).'
4660 cl.RpcServer().add_comment(cl.GetIssue(), comment)
4661
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004662 if os.path.isfile(POSTUPSTREAM_HOOK):
4663 RunCommand([POSTUPSTREAM_HOOK, merge_base], error_ok=True)
4664
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004665 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004666
4667
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004668@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004669def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004670 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004671 parser.add_option('-b', dest='newbranch',
4672 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004673 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004674 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004675 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4676 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004677 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004678 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004679 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004680 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004681 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004682 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004683
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004684
4685 group = optparse.OptionGroup(
4686 parser,
4687 'Options for continuing work on the current issue uploaded from a '
4688 'different clone (e.g. different machine). Must be used independently '
4689 'from the other options. No issue number should be specified, and the '
4690 'branch must have an issue number associated with it')
4691 group.add_option('--reapply', action='store_true', dest='reapply',
4692 help='Reset the branch and reapply the issue.\n'
4693 'CAUTION: This will undo any local changes in this '
4694 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004695
4696 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004697 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004698 parser.add_option_group(group)
4699
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004700 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004701 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004702 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004703 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004704 auth_config = auth.extract_auth_config_from_options(options)
4705
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004706
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004707 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004708 if options.newbranch:
4709 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004710 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004711 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004712
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004713 cl = Changelist(auth_config=auth_config,
4714 codereview=options.forced_codereview)
4715 if not cl.GetIssue():
4716 parser.error('current branch must have an associated issue')
4717
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004718 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004719 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004720 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004721
4722 RunGit(['reset', '--hard', upstream])
4723 if options.pull:
4724 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004725
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004726 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4727 options.directory)
4728
4729 if len(args) != 1 or not args[0]:
4730 parser.error('Must specify issue number or url')
4731
4732 # We don't want uncommitted changes mixed up with the patch.
4733 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004734 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004735
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004736 if options.newbranch:
4737 if options.force:
4738 RunGit(['branch', '-D', options.newbranch],
4739 stderr=subprocess2.PIPE, error_ok=True)
4740 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004741 elif not GetCurrentBranch():
4742 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004743
4744 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4745
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004746 if cl.IsGerrit():
4747 if options.reject:
4748 parser.error('--reject is not supported with Gerrit codereview.')
4749 if options.nocommit:
4750 parser.error('--nocommit is not supported with Gerrit codereview.')
4751 if options.directory:
4752 parser.error('--directory is not supported with Gerrit codereview.')
4753
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004754 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004755 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004756
4757
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004758def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004759 """Fetches the tree status and returns either 'open', 'closed',
4760 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004761 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004762 if url:
4763 status = urllib2.urlopen(url).read().lower()
4764 if status.find('closed') != -1 or status == '0':
4765 return 'closed'
4766 elif status.find('open') != -1 or status == '1':
4767 return 'open'
4768 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004769 return 'unset'
4770
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004771
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004772def GetTreeStatusReason():
4773 """Fetches the tree status from a json url and returns the message
4774 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004775 url = settings.GetTreeStatusUrl()
4776 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004777 connection = urllib2.urlopen(json_url)
4778 status = json.loads(connection.read())
4779 connection.close()
4780 return status['message']
4781
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004782
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004783def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004784 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004785 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004786 status = GetTreeStatus()
4787 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004788 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004789 return 2
4790
vapiera7fbd5a2016-06-16 09:17:49 -07004791 print('The tree is %s' % status)
4792 print()
4793 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004794 if status != 'open':
4795 return 1
4796 return 0
4797
4798
maruel@chromium.org15192402012-09-06 12:38:29 +00004799def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004800 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004801 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004802 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004803 '-b', '--bot', action='append',
4804 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4805 'times to specify multiple builders. ex: '
4806 '"-b win_rel -b win_layout". See '
4807 'the try server waterfall for the builders name and the tests '
4808 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004809 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004810 '-B', '--bucket', default='',
4811 help=('Buildbucket bucket to send the try requests.'))
4812 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004813 '-m', '--master', default='',
4814 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004815 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004816 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004817 help='Revision to use for the try job; default: the revision will '
4818 'be determined by the try recipe that builder runs, which usually '
4819 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004820 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004821 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004822 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004823 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004824 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004825 '--project',
4826 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004827 'in recipe to determine to which repository or directory to '
4828 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004829 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004830 '-p', '--property', dest='properties', action='append', default=[],
4831 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004832 'key2=value2 etc. The value will be treated as '
4833 'json if decodable, or as string otherwise. '
4834 'NOTE: using this may make your try job not usable for CQ, '
4835 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004836 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004837 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4838 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004839 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004840 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004841 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004842 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004843
machenbach@chromium.org45453142015-09-15 08:45:22 +00004844 # Make sure that all properties are prop=value pairs.
4845 bad_params = [x for x in options.properties if '=' not in x]
4846 if bad_params:
4847 parser.error('Got properties with missing "=": %s' % bad_params)
4848
maruel@chromium.org15192402012-09-06 12:38:29 +00004849 if args:
4850 parser.error('Unknown arguments: %s' % args)
4851
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004852 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004853 if not cl.GetIssue():
4854 parser.error('Need to upload first')
4855
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01004856 if cl.IsGerrit():
4857 # HACK: warm up Gerrit change detail cache to save on RPCs.
4858 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
4859
tandriie113dfd2016-10-11 10:20:12 -07004860 error_message = cl.CannotTriggerTryJobReason()
4861 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004862 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004863
borenet6c0efe62016-10-19 08:13:29 -07004864 if options.bucket and options.master:
4865 parser.error('Only one of --bucket and --master may be used.')
4866
qyearsley1fdfcb62016-10-24 13:22:03 -07004867 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004868
qyearsleydd49f942016-10-28 11:57:22 -07004869 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4870 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004871 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004872 if options.verbose:
4873 print('git cl try with no bots now defaults to CQ Dry Run.')
4874 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004875
borenet6c0efe62016-10-19 08:13:29 -07004876 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004877 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004878 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004879 'of bot requires an initial job from a parent (usually a builder). '
4880 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004881 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004882 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004883
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004884 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004885 # TODO(tandrii): Checking local patchset against remote patchset is only
4886 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4887 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07004888 print('Warning: Codereview server has newer patchsets (%s) than most '
4889 'recent upload from local checkout (%s). Did a previous upload '
4890 'fail?\n'
4891 'By default, git cl try uses the latest patchset from '
4892 'codereview, continuing to use patchset %s.\n' %
4893 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07004894
tandrii568043b2016-10-11 07:49:18 -07004895 try:
borenet6c0efe62016-10-19 08:13:29 -07004896 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
4897 patchset)
tandrii568043b2016-10-11 07:49:18 -07004898 except BuildbucketResponseException as ex:
4899 print('ERROR: %s' % ex)
4900 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004901 return 0
4902
4903
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004904def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004905 """Prints info about try jobs associated with current CL."""
4906 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004907 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004908 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004909 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004910 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004911 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004912 '--color', action='store_true', default=setup_color.IS_TTY,
4913 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004914 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004915 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4916 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004917 group.add_option(
4918 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004919 parser.add_option_group(group)
4920 auth.add_auth_options(parser)
4921 options, args = parser.parse_args(args)
4922 if args:
4923 parser.error('Unrecognized args: %s' % ' '.join(args))
4924
4925 auth_config = auth.extract_auth_config_from_options(options)
4926 cl = Changelist(auth_config=auth_config)
4927 if not cl.GetIssue():
4928 parser.error('Need to upload first')
4929
tandrii221ab252016-10-06 08:12:04 -07004930 patchset = options.patchset
4931 if not patchset:
4932 patchset = cl.GetMostRecentPatchset()
4933 if not patchset:
4934 parser.error('Codereview doesn\'t know about issue %s. '
4935 'No access to issue or wrong issue number?\n'
4936 'Either upload first, or pass --patchset explicitely' %
4937 cl.GetIssue())
4938
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004939 # TODO(tandrii): Checking local patchset against remote patchset is only
4940 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4941 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07004942 print('Warning: Codereview server has newer patchsets (%s) than most '
4943 'recent upload from local checkout (%s). Did a previous upload '
4944 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07004945 'By default, git cl try-results uses the latest patchset from '
4946 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07004947 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004948 try:
tandrii221ab252016-10-06 08:12:04 -07004949 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004950 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004951 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004952 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004953 if options.json:
4954 write_try_results_json(options.json, jobs)
4955 else:
4956 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004957 return 0
4958
4959
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004960@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004961def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004962 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004963 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004964 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004965 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004966
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004967 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004968 if args:
4969 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004970 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004971 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004972 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004973 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004974
4975 # Clear configured merge-base, if there is one.
4976 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004977 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004978 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004979 return 0
4980
4981
thestig@chromium.org00858c82013-12-02 23:08:03 +00004982def CMDweb(parser, args):
4983 """Opens the current CL in the web browser."""
4984 _, args = parser.parse_args(args)
4985 if args:
4986 parser.error('Unrecognized args: %s' % ' '.join(args))
4987
4988 issue_url = Changelist().GetIssueURL()
4989 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004990 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004991 return 1
4992
4993 webbrowser.open(issue_url)
4994 return 0
4995
4996
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004997def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004998 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004999 parser.add_option('-d', '--dry-run', action='store_true',
5000 help='trigger in dry run mode')
5001 parser.add_option('-c', '--clear', action='store_true',
5002 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005003 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005004 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005005 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005006 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005007 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005008 if args:
5009 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005010 if options.dry_run and options.clear:
5011 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5012
iannuccie53c9352016-08-17 14:40:40 -07005013 cl = Changelist(auth_config=auth_config, issue=options.issue,
5014 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005015 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005016 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005017 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07005018 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005019 state = _CQState.DRY_RUN
5020 else:
5021 state = _CQState.COMMIT
5022 if not cl.GetIssue():
5023 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005024 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005025 return 0
5026
5027
groby@chromium.org411034a2013-02-26 15:12:01 +00005028def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005029 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005030 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005031 auth.add_auth_options(parser)
5032 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005033 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005034 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005035 if args:
5036 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005037 cl = Changelist(auth_config=auth_config, issue=options.issue,
5038 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005039 # Ensure there actually is an issue to close.
5040 cl.GetDescription()
5041 cl.CloseIssue()
5042 return 0
5043
5044
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005045def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005046 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005047 parser.add_option(
5048 '--stat',
5049 action='store_true',
5050 dest='stat',
5051 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005052 auth.add_auth_options(parser)
5053 options, args = parser.parse_args(args)
5054 auth_config = auth.extract_auth_config_from_options(options)
5055 if args:
5056 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005057
5058 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005059 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005060 # Staged changes would be committed along with the patch from last
5061 # upload, hence counted toward the "last upload" side in the final
5062 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005063 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005064 return 1
5065
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005066 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005067 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005068 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005069 if not issue:
5070 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005071 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005072 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005073
5074 # Create a new branch based on the merge-base
5075 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005076 # Clear cached branch in cl object, to avoid overwriting original CL branch
5077 # properties.
5078 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005079 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005080 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005081 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005082 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005083 return rtn
5084
wychen@chromium.org06928532015-02-03 02:11:29 +00005085 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005086 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005087 cmd = ['git', 'diff']
5088 if options.stat:
5089 cmd.append('--stat')
5090 cmd.extend([TMP_BRANCH, branch, '--'])
5091 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005092 finally:
5093 RunGit(['checkout', '-q', branch])
5094 RunGit(['branch', '-D', TMP_BRANCH])
5095
5096 return 0
5097
5098
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005099def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005100 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005101 parser.add_option(
5102 '--no-color',
5103 action='store_true',
5104 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005105 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005106 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005107 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005108
5109 author = RunGit(['config', 'user.email']).strip() or None
5110
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005111 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005112
5113 if args:
5114 if len(args) > 1:
5115 parser.error('Unknown args')
5116 base_branch = args[0]
5117 else:
5118 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005119 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005120
5121 change = cl.GetChange(base_branch, None)
5122 return owners_finder.OwnersFinder(
5123 [f.LocalPath() for f in
5124 cl.GetChange(base_branch, None).AffectedFiles()],
5125 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005126 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005127 disable_color=options.no_color).run()
5128
5129
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005130def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005131 """Generates a diff command."""
5132 # Generate diff for the current branch's changes.
5133 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005134 upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005135
5136 if args:
5137 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005138 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005139 diff_cmd.append(arg)
5140 else:
5141 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005142
5143 return diff_cmd
5144
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005145
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005146def MatchingFileType(file_name, extensions):
5147 """Returns true if the file name ends with one of the given extensions."""
5148 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005149
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005150
enne@chromium.org555cfe42014-01-29 18:21:39 +00005151@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005152def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005153 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005154 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005155 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005156 parser.add_option('--full', action='store_true',
5157 help='Reformat the full content of all touched files')
5158 parser.add_option('--dry-run', action='store_true',
5159 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005160 parser.add_option('--python', action='store_true',
5161 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005162 parser.add_option('--js', action='store_true',
5163 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005164 parser.add_option('--diff', action='store_true',
5165 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005166 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005167
Daniel Chengc55eecf2016-12-30 03:11:02 -08005168 # Normalize any remaining args against the current path, so paths relative to
5169 # the current directory are still resolved as expected.
5170 args = [os.path.join(os.getcwd(), arg) for arg in args]
5171
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005172 # git diff generates paths against the root of the repository. Change
5173 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005174 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005175 if rel_base_path:
5176 os.chdir(rel_base_path)
5177
digit@chromium.org29e47272013-05-17 17:01:46 +00005178 # Grab the merge-base commit, i.e. the upstream commit of the current
5179 # branch when it was created or the last time it was rebased. This is
5180 # to cover the case where the user may have called "git fetch origin",
5181 # moving the origin branch to a newer commit, but hasn't rebased yet.
5182 upstream_commit = None
5183 cl = Changelist()
5184 upstream_branch = cl.GetUpstreamBranch()
5185 if upstream_branch:
5186 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5187 upstream_commit = upstream_commit.strip()
5188
5189 if not upstream_commit:
5190 DieWithError('Could not find base commit for this branch. '
5191 'Are you in detached state?')
5192
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005193 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5194 diff_output = RunGit(changed_files_cmd)
5195 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005196 # Filter out files deleted by this CL
5197 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005198
Christopher Lamc5ba6922017-01-24 11:19:14 +11005199 if opts.js:
5200 CLANG_EXTS.append('.js')
5201
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005202 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5203 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5204 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005205 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005206
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005207 top_dir = os.path.normpath(
5208 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5209
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005210 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5211 # formatted. This is used to block during the presubmit.
5212 return_value = 0
5213
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005214 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005215 # Locate the clang-format binary in the checkout
5216 try:
5217 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005218 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005219 DieWithError(e)
5220
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005221 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005222 cmd = [clang_format_tool]
5223 if not opts.dry_run and not opts.diff:
5224 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005225 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005226 if opts.diff:
5227 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005228 else:
5229 env = os.environ.copy()
5230 env['PATH'] = str(os.path.dirname(clang_format_tool))
5231 try:
5232 script = clang_format.FindClangFormatScriptInChromiumTree(
5233 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005234 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005235 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005236
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005237 cmd = [sys.executable, script, '-p0']
5238 if not opts.dry_run and not opts.diff:
5239 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005240
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005241 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5242 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005243
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005244 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5245 if opts.diff:
5246 sys.stdout.write(stdout)
5247 if opts.dry_run and len(stdout) > 0:
5248 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005249
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005250 # Similar code to above, but using yapf on .py files rather than clang-format
5251 # on C/C++ files
5252 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005253 yapf_tool = gclient_utils.FindExecutable('yapf')
5254 if yapf_tool is None:
5255 DieWithError('yapf not found in PATH')
5256
5257 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005258 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005259 cmd = [yapf_tool]
5260 if not opts.dry_run and not opts.diff:
5261 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005262 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005263 if opts.diff:
5264 sys.stdout.write(stdout)
5265 else:
5266 # TODO(sbc): yapf --lines mode still has some issues.
5267 # https://github.com/google/yapf/issues/154
5268 DieWithError('--python currently only works with --full')
5269
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005270 # Dart's formatter does not have the nice property of only operating on
5271 # modified chunks, so hard code full.
5272 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005273 try:
5274 command = [dart_format.FindDartFmtToolInChromiumTree()]
5275 if not opts.dry_run and not opts.diff:
5276 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005277 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005278
ppi@chromium.org6593d932016-03-03 15:41:15 +00005279 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005280 if opts.dry_run and stdout:
5281 return_value = 2
5282 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005283 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5284 'found in this checkout. Files in other languages are still '
5285 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005286
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005287 # Format GN build files. Always run on full build files for canonical form.
5288 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005289 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005290 if opts.dry_run or opts.diff:
5291 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005292 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005293 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5294 shell=sys.platform == 'win32',
5295 cwd=top_dir)
5296 if opts.dry_run and gn_ret == 2:
5297 return_value = 2 # Not formatted.
5298 elif opts.diff and gn_ret == 2:
5299 # TODO this should compute and print the actual diff.
5300 print("This change has GN build file diff for " + gn_diff_file)
5301 elif gn_ret != 0:
5302 # For non-dry run cases (and non-2 return values for dry-run), a
5303 # nonzero error code indicates a failure, probably because the file
5304 # doesn't parse.
5305 DieWithError("gn format failed on " + gn_diff_file +
5306 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005307
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005308 metrics_xml_files = [
5309 'tools/metrics/actions/actions.xml',
5310 'tools/metrics/histograms/histograms.xml',
5311 'tools/metrics/rappor/rappor.xml']
5312 for xml_file in metrics_xml_files:
5313 if xml_file in diff_files:
5314 tool_dir = top_dir + '/' + os.path.dirname(xml_file)
5315 cmd = [tool_dir + '/pretty_print.py', '--non-interactive']
5316 if opts.dry_run or opts.diff:
5317 cmd.append('--diff')
5318 stdout = RunCommand(cmd, cwd=top_dir)
5319 if opts.diff:
5320 sys.stdout.write(stdout)
5321 if opts.dry_run and stdout:
5322 return_value = 2 # Not formatted.
5323
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005324 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005325
5326
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005327@subcommand.usage('<codereview url or issue id>')
5328def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005329 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005330 _, args = parser.parse_args(args)
5331
5332 if len(args) != 1:
5333 parser.print_help()
5334 return 1
5335
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005336 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005337 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005338 parser.print_help()
5339 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005340 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005341
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005342 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005343 output = RunGit(['config', '--local', '--get-regexp',
5344 r'branch\..*\.%s' % issueprefix],
5345 error_ok=True)
5346 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005347 if issue == target_issue:
5348 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005349
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005350 branches = []
5351 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005352 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005353 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005354 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005355 return 1
5356 if len(branches) == 1:
5357 RunGit(['checkout', branches[0]])
5358 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005359 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005360 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005361 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005362 which = raw_input('Choose by index: ')
5363 try:
5364 RunGit(['checkout', branches[int(which)]])
5365 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005366 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005367 return 1
5368
5369 return 0
5370
5371
maruel@chromium.org29404b52014-09-08 22:58:00 +00005372def CMDlol(parser, args):
5373 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005374 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005375 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5376 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5377 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005378 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005379 return 0
5380
5381
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005382class OptionParser(optparse.OptionParser):
5383 """Creates the option parse and add --verbose support."""
5384 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005385 optparse.OptionParser.__init__(
5386 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005387 self.add_option(
5388 '-v', '--verbose', action='count', default=0,
5389 help='Use 2 times for more debugging info')
5390
5391 def parse_args(self, args=None, values=None):
5392 options, args = optparse.OptionParser.parse_args(self, args, values)
5393 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005394 logging.basicConfig(
5395 level=levels[min(options.verbose, len(levels) - 1)],
5396 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5397 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005398 return options, args
5399
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005400
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005401def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005402 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005403 print('\nYour python version %s is unsupported, please upgrade.\n' %
5404 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005405 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005406
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005407 # Reload settings.
5408 global settings
5409 settings = Settings()
5410
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005411 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005412 dispatcher = subcommand.CommandDispatcher(__name__)
5413 try:
5414 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005415 except auth.AuthenticationError as e:
5416 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005417 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005418 if e.code != 500:
5419 raise
5420 DieWithError(
5421 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5422 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005423 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005424
5425
5426if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005427 # These affect sys.stdout so do it outside of main() to simplify mocks in
5428 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005429 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005430 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005431 try:
5432 sys.exit(main(sys.argv[1:]))
5433 except KeyboardInterrupt:
5434 sys.stderr.write('interrupted\n')
5435 sys.exit(1)