blob: b604bb19c253c38d30777996d97c2e987ee5fa75 [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02002# Copyright (c) 2013 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 Shyshkalovd8aa49f2017-03-17 16:05:49 +010017import datetime
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +010018import fnmatch
sheyang@google.com6ebaf782015-05-12 19:17:54 +000019import httplib
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +010020import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000021import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000023import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import optparse
25import os
26import re
Andrii Shyshkalov353637c2017-03-14 16:52:18 +010027import shutil
ukai@chromium.org78c4b982012-02-14 02:20:26 +000028import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import sys
Aaron Gable9a03ae02017-11-03 11:31:07 -070030import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000031import textwrap
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000032import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000033import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000034import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000035import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000036import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000037import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000038
39try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080040 import readline # pylint: disable=import-error,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000041except ImportError:
42 pass
43
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000044from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000045from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000046from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000047import auth
skobes6468b902016-10-24 08:45:10 -070048import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000049import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000050import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000051import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000052import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000053import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000054import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000055import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000056import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000057import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000058import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000059import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000060import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000061import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062import scm
Francois Dorayd42c6812017-05-30 15:10:20 -040063import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000064import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000065import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000066import watchlists
67
tandrii7400cf02016-06-21 08:48:07 -070068__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000069
tandrii9d2c7a32016-06-22 03:42:45 -070070COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070071DEFAULT_SERVER = 'https://codereview.chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080072POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000073DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +000074REFS_THAT_ALIAS_TO_OTHER_REFS = {
75 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
76 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
77}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000078
thestig@chromium.org44202a22014-03-11 19:22:18 +000079# Valid extensions for files we want to lint.
80DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
81DEFAULT_LINT_IGNORE_REGEX = r"$^"
82
borenet6c0efe62016-10-19 08:13:29 -070083# Buildbucket master name prefix.
84MASTER_PREFIX = 'master.'
85
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000086# Shortcut since it quickly becomes redundant.
87Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000088
maruel@chromium.orgddd59412011-11-30 14:20:38 +000089# Initialized in main()
90settings = None
91
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010092# Used by tests/git_cl_test.py to add extra logging.
93# Inside the weirdly failing test, add this:
94# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -070095# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010096_IS_BEING_TESTED = False
97
maruel@chromium.orgddd59412011-11-30 14:20:38 +000098
Christopher Lamf732cd52017-01-24 12:40:11 +110099def DieWithError(message, change_desc=None):
100 if change_desc:
101 SaveDescriptionBackup(change_desc)
102
vapiera7fbd5a2016-06-16 09:17:49 -0700103 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000104 sys.exit(1)
105
106
Christopher Lamf732cd52017-01-24 12:40:11 +1100107def SaveDescriptionBackup(change_desc):
108 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
109 print('\nError after CL description prompt -- saving description to %s\n' %
110 backup_path)
111 backup_file = open(backup_path, 'w')
112 backup_file.write(change_desc.description)
113 backup_file.close()
114
115
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000116def GetNoGitPagerEnv():
117 env = os.environ.copy()
118 # 'cat' is a magical git string that disables pagers on all platforms.
119 env['GIT_PAGER'] = 'cat'
120 return env
121
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000122
bsep@chromium.org627d9002016-04-29 00:00:52 +0000123def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000124 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000125 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000126 except subprocess2.CalledProcessError as e:
127 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000128 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000129 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000130 'Command "%s" failed.\n%s' % (
131 ' '.join(args), error_message or e.stdout or ''))
132 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000133
134
135def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000136 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000137 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000138
139
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000140def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000141 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700142 if suppress_stderr:
143 stderr = subprocess2.VOID
144 else:
145 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000146 try:
tandrii5d48c322016-08-18 16:19:37 -0700147 (out, _), code = subprocess2.communicate(['git'] + args,
148 env=GetNoGitPagerEnv(),
149 stdout=subprocess2.PIPE,
150 stderr=stderr)
151 return code, out
152 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900153 logging.debug('Failed running %s', ['git'] + args)
tandrii5d48c322016-08-18 16:19:37 -0700154 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000155
156
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000157def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000158 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000159 return RunGitWithCode(args, suppress_stderr=True)[1]
160
161
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000162def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000163 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000164 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000165 return (version.startswith(prefix) and
166 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000167
168
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000169def BranchExists(branch):
170 """Return True if specified branch exists."""
171 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
172 suppress_stderr=True)
173 return not code
174
175
tandrii2a16b952016-10-19 07:09:44 -0700176def time_sleep(seconds):
177 # Use this so that it can be mocked in tests without interfering with python
178 # system machinery.
179 import time # Local import to discourage others from importing time globally.
180 return time.sleep(seconds)
181
182
maruel@chromium.org90541732011-04-01 17:54:18 +0000183def ask_for_data(prompt):
184 try:
185 return raw_input(prompt)
186 except KeyboardInterrupt:
187 # Hide the exception.
188 sys.exit(1)
189
190
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100191def confirm_or_exit(prefix='', action='confirm'):
192 """Asks user to press enter to continue or press Ctrl+C to abort."""
193 if not prefix or prefix.endswith('\n'):
194 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100195 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100196 mid = ' Press'
197 elif prefix.endswith(' '):
198 mid = 'press'
199 else:
200 mid = ' press'
201 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
202
203
204def ask_for_explicit_yes(prompt):
205 """Returns whether user typed 'y' or 'yes' to confirm the given prompt"""
206 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
207 while True:
208 if 'yes'.startswith(result):
209 return True
210 if 'no'.startswith(result):
211 return False
212 result = ask_for_data('Please, type yes or no: ').lower()
213
214
tandrii5d48c322016-08-18 16:19:37 -0700215def _git_branch_config_key(branch, key):
216 """Helper method to return Git config key for a branch."""
217 assert branch, 'branch name is required to set git config for it'
218 return 'branch.%s.%s' % (branch, key)
219
220
221def _git_get_branch_config_value(key, default=None, value_type=str,
222 branch=False):
223 """Returns git config value of given or current branch if any.
224
225 Returns default in all other cases.
226 """
227 assert value_type in (int, str, bool)
228 if branch is False: # Distinguishing default arg value from None.
229 branch = GetCurrentBranch()
230
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000231 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700232 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000233
tandrii5d48c322016-08-18 16:19:37 -0700234 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700235 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700236 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700237 # git config also has --int, but apparently git config suffers from integer
238 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700239 args.append(_git_branch_config_key(branch, key))
240 code, out = RunGitWithCode(args)
241 if code == 0:
242 value = out.strip()
243 if value_type == int:
244 return int(value)
245 if value_type == bool:
246 return bool(value.lower() == 'true')
247 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000248 return default
249
250
tandrii5d48c322016-08-18 16:19:37 -0700251def _git_set_branch_config_value(key, value, branch=None, **kwargs):
252 """Sets the value or unsets if it's None of a git branch config.
253
254 Valid, though not necessarily existing, branch must be provided,
255 otherwise currently checked out branch is used.
256 """
257 if not branch:
258 branch = GetCurrentBranch()
259 assert branch, 'a branch name OR currently checked out branch is required'
260 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700261 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700262 if value is None:
263 args.append('--unset')
264 elif isinstance(value, bool):
265 args.append('--bool')
266 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700267 else:
tandrii33a46ff2016-08-23 05:53:40 -0700268 # git config also has --int, but apparently git config suffers from integer
269 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700270 value = str(value)
271 args.append(_git_branch_config_key(branch, key))
272 if value is not None:
273 args.append(value)
274 RunGit(args, **kwargs)
275
276
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100277def _get_committer_timestamp(commit):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700278 """Returns Unix timestamp as integer of a committer in a commit.
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100279
280 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
281 """
282 # Git also stores timezone offset, but it only affects visual display,
283 # actual point in time is defined by this timestamp only.
284 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
285
286
287def _git_amend_head(message, committer_timestamp):
288 """Amends commit with new message and desired committer_timestamp.
289
290 Sets committer timezone to UTC.
291 """
292 env = os.environ.copy()
293 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
294 return RunGit(['commit', '--amend', '-m', message], env=env)
295
296
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000297def add_git_similarity(parser):
298 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700299 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000300 help='Sets the percentage that a pair of files need to match in order to'
301 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000302 parser.add_option(
303 '--find-copies', action='store_true',
304 help='Allows git to look for copies.')
305 parser.add_option(
306 '--no-find-copies', action='store_false', dest='find_copies',
307 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000308
309 old_parser_args = parser.parse_args
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700310
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000311 def Parse(args):
312 options, args = old_parser_args(args)
313
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000314 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700315 options.similarity = _git_get_branch_config_value(
316 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000317 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000318 print('Note: Saving similarity of %d%% in git config.'
319 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700320 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000321
iannucci@chromium.org79540052012-10-19 23:15:26 +0000322 options.similarity = max(0, min(options.similarity, 100))
323
324 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700325 options.find_copies = _git_get_branch_config_value(
326 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000327 else:
tandrii5d48c322016-08-18 16:19:37 -0700328 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000329
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000330 return options, args
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700331
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000332 parser.parse_args = Parse
333
334
machenbach@chromium.org45453142015-09-15 08:45:22 +0000335def _get_properties_from_options(options):
336 properties = dict(x.split('=', 1) for x in options.properties)
337 for key, val in properties.iteritems():
338 try:
339 properties[key] = json.loads(val)
340 except ValueError:
341 pass # If a value couldn't be evaluated, treat it as a string.
342 return properties
343
344
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000345def _prefix_master(master):
346 """Convert user-specified master name to full master name.
347
348 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
349 name, while the developers always use shortened master name
350 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
351 function does the conversion for buildbucket migration.
352 """
borenet6c0efe62016-10-19 08:13:29 -0700353 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000354 return master
borenet6c0efe62016-10-19 08:13:29 -0700355 return '%s%s' % (MASTER_PREFIX, master)
356
357
358def _unprefix_master(bucket):
359 """Convert bucket name to shortened master name.
360
361 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
362 name, while the developers always use shortened master name
363 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
364 function does the conversion for buildbucket migration.
365 """
366 if bucket.startswith(MASTER_PREFIX):
367 return bucket[len(MASTER_PREFIX):]
368 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000369
370
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000371def _buildbucket_retry(operation_name, http, *args, **kwargs):
372 """Retries requests to buildbucket service and returns parsed json content."""
373 try_count = 0
374 while True:
375 response, content = http.request(*args, **kwargs)
376 try:
377 content_json = json.loads(content)
378 except ValueError:
379 content_json = None
380
381 # Buildbucket could return an error even if status==200.
382 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000383 error = content_json.get('error')
384 if error.get('code') == 403:
385 raise BuildbucketResponseException(
386 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000387 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000388 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000389 raise BuildbucketResponseException(msg)
390
391 if response.status == 200:
392 if not content_json:
393 raise BuildbucketResponseException(
394 'Buildbucket returns invalid json content: %s.\n'
395 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
396 content)
397 return content_json
398 if response.status < 500 or try_count >= 2:
399 raise httplib2.HttpLib2Error(content)
400
401 # status >= 500 means transient failures.
402 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700403 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000404 try_count += 1
405 assert False, 'unreachable'
406
407
qyearsley1fdfcb62016-10-24 13:22:03 -0700408def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700409 """Returns a dict mapping bucket names to builders and tests,
410 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700411 """
qyearsleydd49f942016-10-28 11:57:22 -0700412 # If no bots are listed, we try to get a set of builders and tests based
413 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700414 if not options.bot:
415 change = changelist.GetChange(
416 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700417 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700418 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700419 change=change,
420 changed_files=change.LocalPaths(),
421 repository_root=settings.GetRoot(),
422 default_presubmit=None,
423 project=None,
424 verbose=options.verbose,
425 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700426 if masters is None:
427 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100428 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700429
qyearsley1fdfcb62016-10-24 13:22:03 -0700430 if options.bucket:
431 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700432 if options.master:
433 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700434
qyearsleydd49f942016-10-28 11:57:22 -0700435 # If bots are listed but no master or bucket, then we need to find out
436 # the corresponding master for each bot.
437 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
438 if error_message:
439 option_parser.error(
440 'Tryserver master cannot be found because: %s\n'
441 'Please manually specify the tryserver master, e.g. '
442 '"-m tryserver.chromium.linux".' % error_message)
443 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700444
445
qyearsley123a4682016-10-26 09:12:17 -0700446def _get_bucket_map_for_builders(builders):
447 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700448 map_url = 'https://builders-map.appspot.com/'
449 try:
qyearsley123a4682016-10-26 09:12:17 -0700450 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700451 except urllib2.URLError as e:
452 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
453 (map_url, e))
454 except ValueError as e:
455 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700456 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700457 return None, 'Failed to build master map.'
458
qyearsley123a4682016-10-26 09:12:17 -0700459 bucket_map = {}
460 for builder in builders:
Nodir Turakulov44e01ff2018-01-25 11:12:30 -0800461 builder_info = builders_map.get(builder, {})
462 if isinstance(builder_info, list):
463 # This is a list of masters, legacy mode.
464 # TODO(nodir): remove this code path.
465 buckets = map(_prefix_master, builder_info)
466 else:
467 buckets = builder_info.get('buckets') or []
468 if not buckets:
469 return None, ('No matching bucket for builder %s.' % builder)
470 if len(buckets) > 1:
471 return None, ('The builder name %s exists in multiple buckets %s.' %
472 (builder, buckets))
473 bucket_map.setdefault(buckets[0], {})[builder] = []
qyearsley123a4682016-10-26 09:12:17 -0700474
475 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700476
477
borenet6c0efe62016-10-19 08:13:29 -0700478def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700479 category='git_cl_try', patchset=None):
qyearsley1fdfcb62016-10-24 13:22:03 -0700480 """Sends a request to Buildbucket to trigger try jobs for a changelist.
481
482 Args:
483 auth_config: AuthConfig for Rietveld.
484 changelist: Changelist that the try jobs are associated with.
485 buckets: A nested dict mapping bucket names to builders to tests.
486 options: Command-line options.
487 """
tandriide281ae2016-10-12 06:02:30 -0700488 assert changelist.GetIssue(), 'CL must be uploaded first'
489 codereview_url = changelist.GetCodereviewServer()
490 assert codereview_url, 'CL must be uploaded first'
491 patchset = patchset or changelist.GetMostRecentPatchset()
492 assert patchset, 'CL must be uploaded first'
493
494 codereview_host = urlparse.urlparse(codereview_url).hostname
495 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000496 http = authenticator.authorize(httplib2.Http())
497 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700498
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000499 buildbucket_put_url = (
500 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000501 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700502 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
503 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
504 hostname=codereview_host,
505 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000506 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700507
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700508 shared_parameters_properties = changelist.GetTryJobProperties(patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700509 shared_parameters_properties['category'] = category
510 if options.clobber:
511 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700512 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700513 if extra_properties:
514 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000515
516 batch_req_body = {'builds': []}
517 print_text = []
518 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700519 for bucket, builders_and_tests in sorted(buckets.iteritems()):
520 print_text.append('Bucket: %s' % bucket)
521 master = None
522 if bucket.startswith(MASTER_PREFIX):
523 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000524 for builder, tests in sorted(builders_and_tests.iteritems()):
525 print_text.append(' %s: %s' % (builder, tests))
526 parameters = {
527 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000528 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100529 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000530 'revision': options.revision,
531 }],
tandrii8c5a3532016-11-04 07:52:02 -0700532 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000533 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000534 if 'presubmit' in builder.lower():
535 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000536 if tests:
537 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700538
539 tags = [
540 'builder:%s' % builder,
541 'buildset:%s' % buildset,
542 'user_agent:git_cl_try',
543 ]
544 if master:
545 parameters['properties']['master'] = master
546 tags.append('master:%s' % master)
547
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000548 batch_req_body['builds'].append(
549 {
550 'bucket': bucket,
551 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000552 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700553 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000554 }
555 )
556
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000557 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700558 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000559 http,
560 buildbucket_put_url,
561 'PUT',
562 body=json.dumps(batch_req_body),
563 headers={'Content-Type': 'application/json'}
564 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000565 print_text.append('To see results here, run: git cl try-results')
566 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700567 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000568
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000569
tandrii221ab252016-10-06 08:12:04 -0700570def fetch_try_jobs(auth_config, changelist, buildbucket_host,
571 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700572 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000573
qyearsley53f48a12016-09-01 10:45:13 -0700574 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000575 """
tandrii221ab252016-10-06 08:12:04 -0700576 assert buildbucket_host
577 assert changelist.GetIssue(), 'CL must be uploaded first'
578 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
579 patchset = patchset or changelist.GetMostRecentPatchset()
580 assert patchset, 'CL must be uploaded first'
581
582 codereview_url = changelist.GetCodereviewServer()
583 codereview_host = urlparse.urlparse(codereview_url).hostname
584 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000585 if authenticator.has_cached_credentials():
586 http = authenticator.authorize(httplib2.Http())
587 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700588 print('Warning: Some results might be missing because %s' %
589 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700590 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000591 http = httplib2.Http()
592
593 http.force_exception_to_status_code = True
594
tandrii221ab252016-10-06 08:12:04 -0700595 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
596 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
597 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000598 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700599 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000600 params = {'tag': 'buildset:%s' % buildset}
601
602 builds = {}
603 while True:
604 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700605 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000606 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700607 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000608 for build in content.get('builds', []):
609 builds[build['id']] = build
610 if 'next_cursor' in content:
611 params['start_cursor'] = content['next_cursor']
612 else:
613 break
614 return builds
615
616
qyearsleyeab3c042016-08-24 09:18:28 -0700617def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000618 """Prints nicely result of fetch_try_jobs."""
619 if not builds:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700620 print('No try jobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000621 return
622
623 # Make a copy, because we'll be modifying builds dictionary.
624 builds = builds.copy()
625 builder_names_cache = {}
626
627 def get_builder(b):
628 try:
629 return builder_names_cache[b['id']]
630 except KeyError:
631 try:
632 parameters = json.loads(b['parameters_json'])
633 name = parameters['builder_name']
634 except (ValueError, KeyError) as error:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700635 print('WARNING: Failed to get builder name for build %s: %s' % (
vapiera7fbd5a2016-06-16 09:17:49 -0700636 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000637 name = None
638 builder_names_cache[b['id']] = name
639 return name
640
641 def get_bucket(b):
642 bucket = b['bucket']
643 if bucket.startswith('master.'):
644 return bucket[len('master.'):]
645 return bucket
646
647 if options.print_master:
648 name_fmt = '%%-%ds %%-%ds' % (
649 max(len(str(get_bucket(b))) for b in builds.itervalues()),
650 max(len(str(get_builder(b))) for b in builds.itervalues()))
651 def get_name(b):
652 return name_fmt % (get_bucket(b), get_builder(b))
653 else:
654 name_fmt = '%%-%ds' % (
655 max(len(str(get_builder(b))) for b in builds.itervalues()))
656 def get_name(b):
657 return name_fmt % get_builder(b)
658
659 def sort_key(b):
660 return b['status'], b.get('result'), get_name(b), b.get('url')
661
662 def pop(title, f, color=None, **kwargs):
663 """Pop matching builds from `builds` dict and print them."""
664
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000665 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000666 colorize = str
667 else:
668 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
669
670 result = []
671 for b in builds.values():
672 if all(b.get(k) == v for k, v in kwargs.iteritems()):
673 builds.pop(b['id'])
674 result.append(b)
675 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700676 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000677 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700678 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000679
680 total = len(builds)
681 pop(status='COMPLETED', result='SUCCESS',
682 title='Successes:', color=Fore.GREEN,
683 f=lambda b: (get_name(b), b.get('url')))
684 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
685 title='Infra Failures:', color=Fore.MAGENTA,
686 f=lambda b: (get_name(b), b.get('url')))
687 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
688 title='Failures:', color=Fore.RED,
689 f=lambda b: (get_name(b), b.get('url')))
690 pop(status='COMPLETED', result='CANCELED',
691 title='Canceled:', color=Fore.MAGENTA,
692 f=lambda b: (get_name(b),))
693 pop(status='COMPLETED', result='FAILURE',
694 failure_reason='INVALID_BUILD_DEFINITION',
695 title='Wrong master/builder name:', color=Fore.MAGENTA,
696 f=lambda b: (get_name(b),))
697 pop(status='COMPLETED', result='FAILURE',
698 title='Other failures:',
699 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
700 pop(status='COMPLETED',
701 title='Other finished:',
702 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
703 pop(status='STARTED',
704 title='Started:', color=Fore.YELLOW,
705 f=lambda b: (get_name(b), b.get('url')))
706 pop(status='SCHEDULED',
707 title='Scheduled:',
708 f=lambda b: (get_name(b), 'id=%s' % b['id']))
709 # The last section is just in case buildbucket API changes OR there is a bug.
710 pop(title='Other:',
711 f=lambda b: (get_name(b), 'id=%s' % b['id']))
712 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700713 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000714
715
qyearsley53f48a12016-09-01 10:45:13 -0700716def write_try_results_json(output_file, builds):
717 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
718
719 The input |builds| dict is assumed to be generated by Buildbucket.
720 Buildbucket documentation: http://goo.gl/G0s101
721 """
722
723 def convert_build_dict(build):
Quinten Yearsleya563d722017-12-11 16:36:54 -0800724 """Extracts some of the information from one build dict."""
725 parameters = json.loads(build.get('parameters_json', '{}')) or {}
qyearsley53f48a12016-09-01 10:45:13 -0700726 return {
727 'buildbucket_id': build.get('id'),
qyearsley53f48a12016-09-01 10:45:13 -0700728 'bucket': build.get('bucket'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800729 'builder_name': parameters.get('builder_name'),
730 'created_ts': build.get('created_ts'),
731 'experimental': build.get('experimental'),
qyearsley53f48a12016-09-01 10:45:13 -0700732 'failure_reason': build.get('failure_reason'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800733 'result': build.get('result'),
734 'status': build.get('status'),
735 'tags': build.get('tags'),
qyearsley53f48a12016-09-01 10:45:13 -0700736 'url': build.get('url'),
737 }
738
739 converted = []
740 for _, build in sorted(builds.items()):
741 converted.append(convert_build_dict(build))
742 write_json(output_file, converted)
743
744
iannucci@chromium.org79540052012-10-19 23:15:26 +0000745def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000746 """Prints statistics about the change to the user."""
747 # --no-ext-diff is broken in some versions of Git, so try to work around
748 # this by overriding the environment (but there is still a problem if the
749 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000750 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000751 if 'GIT_EXTERNAL_DIFF' in env:
752 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000753
754 if find_copies:
scottmgb84b5e32016-11-10 09:25:33 -0800755 similarity_options = ['-l100000', '-C%s' % similarity]
iannucci@chromium.org79540052012-10-19 23:15:26 +0000756 else:
757 similarity_options = ['-M%s' % similarity]
758
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000759 try:
760 stdout = sys.stdout.fileno()
761 except AttributeError:
762 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000763 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000764 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000765 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000766 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000767
768
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000769class BuildbucketResponseException(Exception):
770 pass
771
772
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000773class Settings(object):
774 def __init__(self):
775 self.default_server = None
776 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000777 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000778 self.tree_status_url = None
779 self.viewvc_url = None
780 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000781 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000782 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000783 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000784 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000785 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000786 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000787
788 def LazyUpdateIfNeeded(self):
789 """Updates the settings from a codereview.settings file, if available."""
790 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000791 # The only value that actually changes the behavior is
792 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000793 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000794 error_ok=True
795 ).strip().lower()
796
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000797 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000798 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000799 LoadCodereviewSettingsFromFile(cr_settings_file)
800 self.updated = True
801
802 def GetDefaultServerUrl(self, error_ok=False):
803 if not self.default_server:
804 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000805 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000806 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000807 if error_ok:
808 return self.default_server
809 if not self.default_server:
810 error_message = ('Could not find settings file. You must configure '
811 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000812 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000813 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000814 return self.default_server
815
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000816 @staticmethod
817 def GetRelativeRoot():
818 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000819
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000820 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000821 if self.root is None:
822 self.root = os.path.abspath(self.GetRelativeRoot())
823 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000824
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000825 def GetGitMirror(self, remote='origin'):
826 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000827 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000828 if not os.path.isdir(local_url):
829 return None
830 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
831 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100832 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 16:44:13 +0100833 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000834 if mirror.exists():
835 return mirror
836 return None
837
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000838 def GetTreeStatusUrl(self, error_ok=False):
839 if not self.tree_status_url:
840 error_message = ('You must configure your tree status URL by running '
841 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000842 self.tree_status_url = self._GetRietveldConfig(
843 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000844 return self.tree_status_url
845
846 def GetViewVCUrl(self):
847 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000848 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000849 return self.viewvc_url
850
rmistry@google.com90752582014-01-14 21:04:50 +0000851 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000852 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000853
rmistry@google.com78948ed2015-07-08 23:09:57 +0000854 def GetIsSkipDependencyUpload(self, branch_name):
855 """Returns true if specified branch should skip dep uploads."""
856 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
857 error_ok=True)
858
rmistry@google.com5626a922015-02-26 14:03:30 +0000859 def GetRunPostUploadHook(self):
860 run_post_upload_hook = self._GetRietveldConfig(
861 'run-post-upload-hook', error_ok=True)
862 return run_post_upload_hook == "True"
863
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000864 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000865 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000866
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000867 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000868 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000869
ukai@chromium.orge8077812012-02-03 03:41:46 +0000870 def GetIsGerrit(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700871 """Return true if this repo is associated with gerrit code review system."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000872 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700873 self.is_gerrit = (
874 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000875 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.
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700953 print('WARNING: Failed to fetch project config for %s: %s' %
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100954 (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',
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +0100978 'infra/experimental',
Edward Lemur32357d32017-09-11 20:22:45 +0200979 # For webrtc.googlesource.com/src.
980 'src',
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100981 ]
982
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100983 assert remote_ref and remote_ref.startswith('refs/'), remote_ref
984 url_parts = urlparse.urlparse(remote_url)
985 project_name = url_parts.path.lstrip('/').rstrip('git./')
986 for known in KNOWN_PROJECTS_WHITELIST:
987 if project_name.endswith(known):
988 break
989 else:
990 # Early exit to avoid extra fetches for repos that aren't using Git
991 # Numberer.
992 return False
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100993
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100994 with _get_gerrit_project_config_file(remote_url) as project_config_file:
995 if project_config_file is None:
996 # Failed to fetch project.config, which shouldn't happen on open source
997 # repos KNOWN_PROJECTS_WHITELIST.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100998 return False
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100999 def get_opts(x):
1000 code, out = RunGitWithCode(
1001 ['config', '-f', project_config_file, '--get-all',
1002 'plugin.git-numberer.validate-%s-refglob' % x])
1003 if code == 0:
1004 return out.strip().splitlines()
1005 return []
1006 enabled, disabled = map(get_opts, ['enabled', 'disabled'])
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001007
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01001008 logging.info('validator config enabled %s disabled %s refglobs for '
1009 '(this ref: %s)', enabled, disabled, remote_ref)
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +00001010
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01001011 def match_refglobs(refglobs):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001012 for refglob in refglobs:
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01001013 if remote_ref == refglob or fnmatch.fnmatch(remote_ref, refglob):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001014 return True
1015 return False
1016
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01001017 if match_refglobs(disabled):
1018 return False
1019 return match_refglobs(enabled)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001020
1021
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001022def ShortBranchName(branch):
1023 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001024 return branch.replace('refs/heads/', '', 1)
1025
1026
1027def GetCurrentBranchRef():
1028 """Returns branch ref (e.g., refs/heads/master) or None."""
1029 return RunGit(['symbolic-ref', 'HEAD'],
1030 stderr=subprocess2.VOID, error_ok=True).strip() or None
1031
1032
1033def GetCurrentBranch():
1034 """Returns current branch or None.
1035
1036 For refs/heads/* branches, returns just last part. For others, full ref.
1037 """
1038 branchref = GetCurrentBranchRef()
1039 if branchref:
1040 return ShortBranchName(branchref)
1041 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001042
1043
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001044class _CQState(object):
1045 """Enum for states of CL with respect to Commit Queue."""
1046 NONE = 'none'
1047 DRY_RUN = 'dry_run'
1048 COMMIT = 'commit'
1049
1050 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1051
1052
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001053class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001054 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001055 self.issue = issue
1056 self.patchset = patchset
1057 self.hostname = hostname
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001058 assert codereview in (None, 'rietveld', 'gerrit')
1059 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001060
1061 @property
1062 def valid(self):
1063 return self.issue is not None
1064
1065
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001066def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001067 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1068 fail_result = _ParsedIssueNumberArgument()
1069
1070 if arg.isdigit():
Aaron Gableaee6c852017-06-26 12:49:01 -07001071 return _ParsedIssueNumberArgument(issue=int(arg), codereview=codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001072 if not arg.startswith('http'):
1073 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -07001074
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001075 url = gclient_utils.UpgradeToHttps(arg)
1076 try:
1077 parsed_url = urlparse.urlparse(url)
1078 except ValueError:
1079 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001080
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001081 if codereview is not None:
1082 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
1083 return parsed or fail_result
1084
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001085 results = {}
1086 for name, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1087 parsed = cls.ParseIssueURL(parsed_url)
1088 if parsed is not None:
1089 results[name] = parsed
1090
1091 if not results:
1092 return fail_result
1093 if len(results) == 1:
1094 return results.values()[0]
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001095
1096 if parsed_url.netloc and parsed_url.netloc.split('.')[0].endswith('-review'):
1097 # This is likely Gerrit.
1098 return results['gerrit']
1099 # Choose Rietveld as before if URL can parsed by either.
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001100 return results['rietveld']
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001101
1102
Aaron Gablea45ee112016-11-22 15:14:38 -08001103class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001104 def __init__(self, issue, url):
1105 self.issue = issue
1106 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001107 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001108
1109 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001110 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001111 self.issue, self.url)
1112
1113
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001114_CommentSummary = collections.namedtuple(
1115 '_CommentSummary', ['date', 'message', 'sender',
1116 # TODO(tandrii): these two aren't known in Gerrit.
1117 'approval', 'disapproval'])
1118
1119
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001120class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001121 """Changelist works with one changelist in local branch.
1122
1123 Supports two codereview backends: Rietveld or Gerrit, selected at object
1124 creation.
1125
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001126 Notes:
1127 * Not safe for concurrent multi-{thread,process} use.
1128 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001129 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001130 """
1131
1132 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1133 """Create a new ChangeList instance.
1134
1135 If issue is given, the codereview must be given too.
1136
1137 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1138 Otherwise, it's decided based on current configuration of the local branch,
1139 with default being 'rietveld' for backwards compatibility.
1140 See _load_codereview_impl for more details.
1141
1142 **kwargs will be passed directly to codereview implementation.
1143 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001144 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001145 global settings
1146 if not settings:
1147 # Happens when git_cl.py is used as a utility library.
1148 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001149
1150 if issue:
1151 assert codereview, 'codereview must be known, if issue is known'
1152
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001153 self.branchref = branchref
1154 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001155 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001156 self.branch = ShortBranchName(self.branchref)
1157 else:
1158 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001159 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001160 self.lookedup_issue = False
1161 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001162 self.has_description = False
1163 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001164 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001165 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001166 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001167 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001168 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001169
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001170 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001171 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001172 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001173 assert self._codereview_impl
1174 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001175
1176 def _load_codereview_impl(self, codereview=None, **kwargs):
1177 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001178 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1179 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1180 self._codereview = codereview
1181 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001182 return
1183
1184 # Automatic selection based on issue number set for a current branch.
1185 # Rietveld takes precedence over Gerrit.
1186 assert not self.issue
1187 # Whether we find issue or not, we are doing the lookup.
1188 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001189 if self.GetBranch():
1190 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1191 issue = _git_get_branch_config_value(
1192 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1193 if issue:
1194 self._codereview = codereview
1195 self._codereview_impl = cls(self, **kwargs)
1196 self.issue = int(issue)
1197 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001198
1199 # No issue is set for this branch, so decide based on repo-wide settings.
1200 return self._load_codereview_impl(
1201 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1202 **kwargs)
1203
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001204 def IsGerrit(self):
1205 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001206
1207 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001208 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001209
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001210 The return value is a string suitable for passing to git cl with the --cc
1211 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001212 """
1213 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001214 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001215 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001216 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1217 return self.cc
1218
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001219 def GetCCListWithoutDefault(self):
1220 """Return the users cc'd on this CL excluding default ones."""
1221 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001222 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001223 return self.cc
1224
Daniel Cheng7227d212017-11-17 08:12:37 -08001225 def ExtendCC(self, more_cc):
1226 """Extends the list of users to cc on this CL based on the changed files."""
1227 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001228
1229 def GetBranch(self):
1230 """Returns the short branch name, e.g. 'master'."""
1231 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001232 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001233 if not branchref:
1234 return None
1235 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001236 self.branch = ShortBranchName(self.branchref)
1237 return self.branch
1238
1239 def GetBranchRef(self):
1240 """Returns the full branch name, e.g. 'refs/heads/master'."""
1241 self.GetBranch() # Poke the lazy loader.
1242 return self.branchref
1243
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001244 def ClearBranch(self):
1245 """Clears cached branch data of this object."""
1246 self.branch = self.branchref = None
1247
tandrii5d48c322016-08-18 16:19:37 -07001248 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1249 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1250 kwargs['branch'] = self.GetBranch()
1251 return _git_get_branch_config_value(key, default, **kwargs)
1252
1253 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1254 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1255 assert self.GetBranch(), (
1256 'this CL must have an associated branch to %sset %s%s' %
1257 ('un' if value is None else '',
1258 key,
1259 '' if value is None else ' to %r' % value))
1260 kwargs['branch'] = self.GetBranch()
1261 return _git_set_branch_config_value(key, value, **kwargs)
1262
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001263 @staticmethod
1264 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001265 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001266 e.g. 'origin', 'refs/heads/master'
1267 """
1268 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001269 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1270
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001271 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001272 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001273 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001274 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1275 error_ok=True).strip()
1276 if upstream_branch:
1277 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001278 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001279 # Else, try to guess the origin remote.
1280 remote_branches = RunGit(['branch', '-r']).split()
1281 if 'origin/master' in remote_branches:
1282 # Fall back on origin/master if it exits.
1283 remote = 'origin'
1284 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001285 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001286 DieWithError(
1287 'Unable to determine default branch to diff against.\n'
1288 'Either pass complete "git diff"-style arguments, like\n'
1289 ' git cl upload origin/master\n'
1290 'or verify this branch is set up to track another \n'
1291 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001292
1293 return remote, upstream_branch
1294
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001295 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001296 upstream_branch = self.GetUpstreamBranch()
1297 if not BranchExists(upstream_branch):
1298 DieWithError('The upstream for the current branch (%s) does not exist '
1299 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001300 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001301 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001302
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001303 def GetUpstreamBranch(self):
1304 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001305 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001306 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001307 upstream_branch = upstream_branch.replace('refs/heads/',
1308 'refs/remotes/%s/' % remote)
1309 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1310 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001311 self.upstream_branch = upstream_branch
1312 return self.upstream_branch
1313
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001314 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001315 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001316 remote, branch = None, self.GetBranch()
1317 seen_branches = set()
1318 while branch not in seen_branches:
1319 seen_branches.add(branch)
1320 remote, branch = self.FetchUpstreamTuple(branch)
1321 branch = ShortBranchName(branch)
1322 if remote != '.' or branch.startswith('refs/remotes'):
1323 break
1324 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001325 remotes = RunGit(['remote'], error_ok=True).split()
1326 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001327 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001328 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001329 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001330 logging.warn('Could not determine which remote this change is '
1331 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001332 else:
1333 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001334 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001335 branch = 'HEAD'
1336 if branch.startswith('refs/remotes'):
1337 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001338 elif branch.startswith('refs/branch-heads/'):
1339 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001340 else:
1341 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001342 return self._remote
1343
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001344 def GitSanityChecks(self, upstream_git_obj):
1345 """Checks git repo status and ensures diff is from local commits."""
1346
sbc@chromium.org79706062015-01-14 21:18:12 +00001347 if upstream_git_obj is None:
1348 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001349 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001350 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001351 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001352 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001353 return False
1354
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001355 # Verify the commit we're diffing against is in our current branch.
1356 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1357 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1358 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001359 print('ERROR: %s is not in the current branch. You may need to rebase '
1360 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001361 return False
1362
1363 # List the commits inside the diff, and verify they are all local.
1364 commits_in_diff = RunGit(
1365 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1366 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1367 remote_branch = remote_branch.strip()
1368 if code != 0:
1369 _, remote_branch = self.GetRemoteBranch()
1370
1371 commits_in_remote = RunGit(
1372 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1373
1374 common_commits = set(commits_in_diff) & set(commits_in_remote)
1375 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001376 print('ERROR: Your diff contains %d commits already in %s.\n'
1377 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1378 'the diff. If you are using a custom git flow, you can override'
1379 ' the reference used for this check with "git config '
1380 'gitcl.remotebranch <git-ref>".' % (
1381 len(common_commits), remote_branch, upstream_git_obj),
1382 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001383 return False
1384 return True
1385
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001386 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001387 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001388
1389 Returns None if it is not set.
1390 """
tandrii5d48c322016-08-18 16:19:37 -07001391 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001392
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001393 def GetRemoteUrl(self):
1394 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1395
1396 Returns None if there is no remote.
1397 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001398 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001399 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1400
1401 # If URL is pointing to a local directory, it is probably a git cache.
1402 if os.path.isdir(url):
1403 url = RunGit(['config', 'remote.%s.url' % remote],
1404 error_ok=True,
1405 cwd=url).strip()
1406 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001407
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001408 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001409 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001410 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001411 self.issue = self._GitGetBranchConfigValue(
1412 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001413 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001414 return self.issue
1415
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001416 def GetIssueURL(self):
1417 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001418 issue = self.GetIssue()
1419 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001420 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001421 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001422
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001423 def GetDescription(self, pretty=False, force=False):
1424 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001425 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001426 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001427 self.has_description = True
1428 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001429 # Set width to 72 columns + 2 space indent.
1430 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001431 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001432 lines = self.description.splitlines()
1433 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001434 return self.description
1435
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001436 def GetDescriptionFooters(self):
1437 """Returns (non_footer_lines, footers) for the commit message.
1438
1439 Returns:
1440 non_footer_lines (list(str)) - Simple list of description lines without
1441 any footer. The lines do not contain newlines, nor does the list contain
1442 the empty line between the message and the footers.
1443 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1444 [("Change-Id", "Ideadbeef...."), ...]
1445 """
1446 raw_description = self.GetDescription()
1447 msg_lines, _, footers = git_footers.split_footers(raw_description)
1448 if footers:
1449 msg_lines = msg_lines[:len(msg_lines)-1]
1450 return msg_lines, footers
1451
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001452 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001453 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001454 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001455 self.patchset = self._GitGetBranchConfigValue(
1456 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001457 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001458 return self.patchset
1459
1460 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001461 """Set this branch's patchset. If patchset=0, clears the patchset."""
1462 assert self.GetBranch()
1463 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001464 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001465 else:
1466 self.patchset = int(patchset)
1467 self._GitSetBranchConfigValue(
1468 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001469
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001470 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001471 """Set this branch's issue. If issue isn't given, clears the issue."""
1472 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001473 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001474 issue = int(issue)
1475 self._GitSetBranchConfigValue(
1476 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001477 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001478 codereview_server = self._codereview_impl.GetCodereviewServer()
1479 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001480 self._GitSetBranchConfigValue(
1481 self._codereview_impl.CodereviewServerConfigKey(),
1482 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001483 else:
tandrii5d48c322016-08-18 16:19:37 -07001484 # Reset all of these just to be clean.
1485 reset_suffixes = [
1486 'last-upload-hash',
1487 self._codereview_impl.IssueConfigKey(),
1488 self._codereview_impl.PatchsetConfigKey(),
1489 self._codereview_impl.CodereviewServerConfigKey(),
1490 ] + self._PostUnsetIssueProperties()
1491 for prop in reset_suffixes:
1492 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001493 msg = RunGit(['log', '-1', '--format=%B']).strip()
1494 if msg and git_footers.get_footer_change_id(msg):
1495 print('WARNING: The change patched into this branch has a Change-Id. '
1496 'Removing it.')
1497 RunGit(['commit', '--amend', '-m',
1498 git_footers.remove_footer(msg, 'Change-Id')])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001499 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001500 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001501
dnjba1b0f32016-09-02 12:37:42 -07001502 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001503 if not self.GitSanityChecks(upstream_branch):
1504 DieWithError('\nGit sanity check failure')
1505
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001506 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001507 if not root:
1508 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001509 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001510
1511 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001512 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001513 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001514 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001515 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001516 except subprocess2.CalledProcessError:
1517 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001518 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001519 'This branch probably doesn\'t exist anymore. To reset the\n'
1520 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001521 ' git branch --set-upstream-to origin/master %s\n'
1522 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001523 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001524
maruel@chromium.org52424302012-08-29 15:14:30 +00001525 issue = self.GetIssue()
1526 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001527 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001528 description = self.GetDescription()
1529 else:
1530 # If the change was never uploaded, use the log messages of all commits
1531 # up to the branch point, as git cl upload will prefill the description
1532 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001533 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1534 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001535
1536 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001537 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001538 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001539 name,
1540 description,
1541 absroot,
1542 files,
1543 issue,
1544 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001545 author,
1546 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001547
dsansomee2d6fd92016-09-08 00:10:47 -07001548 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001549 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001550 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001551 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001552
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001553 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1554 """Sets the description for this CL remotely.
1555
1556 You can get description_lines and footers with GetDescriptionFooters.
1557
1558 Args:
1559 description_lines (list(str)) - List of CL description lines without
1560 newline characters.
1561 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1562 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1563 `List-Of-Tokens`). It will be case-normalized so that each token is
1564 title-cased.
1565 """
1566 new_description = '\n'.join(description_lines)
1567 if footers:
1568 new_description += '\n'
1569 for k, v in footers:
1570 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1571 if not git_footers.FOOTER_PATTERN.match(foot):
1572 raise ValueError('Invalid footer %r' % foot)
1573 new_description += foot + '\n'
1574 self.UpdateDescription(new_description, force)
1575
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001576 def RunHook(self, committing, may_prompt, verbose, change):
1577 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1578 try:
1579 return presubmit_support.DoPresubmitChecks(change, committing,
1580 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1581 default_presubmit=None, may_prompt=may_prompt,
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001582 rietveld_obj=self._codereview_impl.GetRietveldObjForPresubmit(),
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001583 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001584 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001585 DieWithError(
1586 ('%s\nMaybe your depot_tools is out of date?\n'
1587 'If all fails, contact maruel@') % e)
1588
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001589 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1590 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001591 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1592 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001593 else:
1594 # Assume url.
1595 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1596 urlparse.urlparse(issue_arg))
1597 if not parsed_issue_arg or not parsed_issue_arg.valid:
1598 DieWithError('Failed to parse issue argument "%s". '
1599 'Must be an issue number or a valid URL.' % issue_arg)
1600 return self._codereview_impl.CMDPatchWithParsedIssue(
Aaron Gable62619a32017-06-16 08:22:09 -07001601 parsed_issue_arg, reject, nocommit, directory, False)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001602
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001603 def CMDUpload(self, options, git_diff_args, orig_args):
1604 """Uploads a change to codereview."""
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001605 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001606 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001607 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001608 else:
1609 if self.GetBranch() is None:
1610 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1611
1612 # Default to diffing against common ancestor of upstream branch
1613 base_branch = self.GetCommonAncestorWithUpstream()
1614 git_diff_args = [base_branch, 'HEAD']
1615
Aaron Gablec4c40d12017-05-22 11:49:53 -07001616 # Warn about Rietveld deprecation for initial uploads to Rietveld.
1617 if not self.IsGerrit() and not self.GetIssue():
1618 print('=====================================')
1619 print('NOTICE: Rietveld is being deprecated. '
1620 'You can upload changes to Gerrit with')
1621 print(' git cl upload --gerrit')
1622 print('or set Gerrit to be your default code review tool with')
1623 print(' git config gerrit.host true')
1624 print('=====================================')
1625
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001626 # Fast best-effort checks to abort before running potentially
1627 # expensive hooks if uploading is likely to fail anyway. Passing these
1628 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001629 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001630 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001631
1632 # Apply watchlists on upload.
1633 change = self.GetChange(base_branch, None)
1634 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1635 files = [f.LocalPath() for f in change.AffectedFiles()]
1636 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001637 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001638
1639 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001640 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001641 # Set the reviewer list now so that presubmit checks can access it.
1642 change_description = ChangeDescription(change.FullDescriptionText())
1643 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001644 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001645 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001646 change)
1647 change.SetDescriptionText(change_description.description)
1648 hook_results = self.RunHook(committing=False,
1649 may_prompt=not options.force,
1650 verbose=options.verbose,
1651 change=change)
1652 if not hook_results.should_continue():
1653 return 1
1654 if not options.reviewers and hook_results.reviewers:
1655 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001656 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001657
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001658 # TODO(tandrii): Checking local patchset against remote patchset is only
1659 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1660 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001661 latest_patchset = self.GetMostRecentPatchset()
1662 local_patchset = self.GetPatchset()
1663 if (latest_patchset and local_patchset and
1664 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001665 print('The last upload made from this repository was patchset #%d but '
1666 'the most recent patchset on the server is #%d.'
1667 % (local_patchset, latest_patchset))
1668 print('Uploading will still work, but if you\'ve uploaded to this '
1669 'issue from another machine or branch the patch you\'re '
1670 'uploading now might not include those changes.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001671 confirm_or_exit(action='upload')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001672
1673 print_stats(options.similarity, options.find_copies, git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001674 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001675 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001676 if options.use_commit_queue:
1677 self.SetCQState(_CQState.COMMIT)
1678 elif options.cq_dry_run:
1679 self.SetCQState(_CQState.DRY_RUN)
1680
tandrii5d48c322016-08-18 16:19:37 -07001681 _git_set_branch_config_value('last-upload-hash',
1682 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001683 # Run post upload hooks, if specified.
1684 if settings.GetRunPostUploadHook():
1685 presubmit_support.DoPostUploadExecuter(
1686 change,
1687 self,
1688 settings.GetRoot(),
1689 options.verbose,
1690 sys.stdout)
1691
1692 # Upload all dependencies if specified.
1693 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001694 print()
1695 print('--dependencies has been specified.')
1696 print('All dependent local branches will be re-uploaded.')
1697 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001698 # Remove the dependencies flag from args so that we do not end up in a
1699 # loop.
1700 orig_args.remove('--dependencies')
1701 ret = upload_branch_deps(self, orig_args)
1702 return ret
1703
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001704 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001705 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001706
1707 Issue must have been already uploaded and known.
1708 """
1709 assert new_state in _CQState.ALL_STATES
1710 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001711 try:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001712 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 13:22:03 -07001713 return 0
1714 except KeyboardInterrupt:
1715 raise
1716 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001717 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001718 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001719 ' * Your project has no CQ,\n'
1720 ' * You don\'t have permission to change the CQ state,\n'
1721 ' * There\'s a bug in this code (see stack trace below).\n'
1722 'Consider specifying which bots to trigger manually or asking your '
1723 'project owners for permissions or contacting Chrome Infra at:\n'
1724 'https://www.chromium.org/infra\n\n' %
1725 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001726 # Still raise exception so that stack trace is printed.
1727 raise
1728
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001729 # Forward methods to codereview specific implementation.
1730
Aaron Gable636b13f2017-07-14 10:42:48 -07001731 def AddComment(self, message, publish=None):
1732 return self._codereview_impl.AddComment(message, publish=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001733
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001734 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001735 """Returns list of _CommentSummary for each comment.
1736
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001737 args:
1738 readable: determines whether the output is designed for a human or a machine
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001739 """
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001740 return self._codereview_impl.GetCommentsSummary(readable)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001741
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001742 def CloseIssue(self):
1743 return self._codereview_impl.CloseIssue()
1744
1745 def GetStatus(self):
1746 return self._codereview_impl.GetStatus()
1747
1748 def GetCodereviewServer(self):
1749 return self._codereview_impl.GetCodereviewServer()
1750
tandriide281ae2016-10-12 06:02:30 -07001751 def GetIssueOwner(self):
1752 """Get owner from codereview, which may differ from this checkout."""
1753 return self._codereview_impl.GetIssueOwner()
1754
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001755 def GetMostRecentPatchset(self):
1756 return self._codereview_impl.GetMostRecentPatchset()
1757
tandriide281ae2016-10-12 06:02:30 -07001758 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001759 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriide281ae2016-10-12 06:02:30 -07001760 return self._codereview_impl.CannotTriggerTryJobReason()
1761
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001762 def GetTryJobProperties(self, patchset=None):
1763 """Returns dictionary of properties to launch try job."""
1764 return self._codereview_impl.GetTryJobProperties(patchset=patchset)
tandrii8c5a3532016-11-04 07:52:02 -07001765
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001766 def __getattr__(self, attr):
1767 # This is because lots of untested code accesses Rietveld-specific stuff
1768 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001769 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001770 # Note that child method defines __getattr__ as well, and forwards it here,
1771 # because _RietveldChangelistImpl is not cleaned up yet, and given
1772 # deprecation of Rietveld, it should probably be just removed.
1773 # Until that time, avoid infinite recursion by bypassing __getattr__
1774 # of implementation class.
1775 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001776
1777
1778class _ChangelistCodereviewBase(object):
1779 """Abstract base class encapsulating codereview specifics of a changelist."""
1780 def __init__(self, changelist):
1781 self._changelist = changelist # instance of Changelist
1782
1783 def __getattr__(self, attr):
1784 # Forward methods to changelist.
1785 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1786 # _RietveldChangelistImpl to avoid this hack?
1787 return getattr(self._changelist, attr)
1788
1789 def GetStatus(self):
1790 """Apply a rough heuristic to give a simple summary of an issue's review
1791 or CQ status, assuming adherence to a common workflow.
1792
1793 Returns None if no issue for this branch, or specific string keywords.
1794 """
1795 raise NotImplementedError()
1796
1797 def GetCodereviewServer(self):
1798 """Returns server URL without end slash, like "https://codereview.com"."""
1799 raise NotImplementedError()
1800
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001801 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001802 """Fetches and returns description from the codereview server."""
1803 raise NotImplementedError()
1804
tandrii5d48c322016-08-18 16:19:37 -07001805 @classmethod
1806 def IssueConfigKey(cls):
1807 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001808 raise NotImplementedError()
1809
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001810 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001811 def PatchsetConfigKey(cls):
1812 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001813 raise NotImplementedError()
1814
tandrii5d48c322016-08-18 16:19:37 -07001815 @classmethod
1816 def CodereviewServerConfigKey(cls):
1817 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001818 raise NotImplementedError()
1819
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001820 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001821 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001822 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001823
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001824 def GetRietveldObjForPresubmit(self):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001825 # This is an unfortunate Rietveld-embeddedness in presubmit.
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001826 # For non-Rietveld code reviews, this probably should return a dummy object.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001827 raise NotImplementedError()
1828
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001829 def GetGerritObjForPresubmit(self):
1830 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1831 return None
1832
dsansomee2d6fd92016-09-08 00:10:47 -07001833 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001834 """Update the description on codereview site."""
1835 raise NotImplementedError()
1836
Aaron Gable636b13f2017-07-14 10:42:48 -07001837 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001838 """Posts a comment to the codereview site."""
1839 raise NotImplementedError()
1840
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001841 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001842 raise NotImplementedError()
1843
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001844 def CloseIssue(self):
1845 """Closes the issue."""
1846 raise NotImplementedError()
1847
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001848 def GetMostRecentPatchset(self):
1849 """Returns the most recent patchset number from the codereview site."""
1850 raise NotImplementedError()
1851
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001852 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07001853 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001854 """Fetches and applies the issue.
1855
1856 Arguments:
1857 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1858 reject: if True, reject the failed patch instead of switching to 3-way
1859 merge. Rietveld only.
1860 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1861 only.
1862 directory: switch to directory before applying the patch. Rietveld only.
Aaron Gable62619a32017-06-16 08:22:09 -07001863 force: if true, overwrites existing local state.
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001864 """
1865 raise NotImplementedError()
1866
1867 @staticmethod
1868 def ParseIssueURL(parsed_url):
1869 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1870 failed."""
1871 raise NotImplementedError()
1872
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001873 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001874 """Best effort check that user is authenticated with codereview server.
1875
1876 Arguments:
1877 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001878 refresh: whether to attempt to refresh credentials. Ignored if not
1879 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001880 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001881 raise NotImplementedError()
1882
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001883 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001884 """Best effort check that uploading isn't supposed to fail for predictable
1885 reasons.
1886
1887 This method should raise informative exception if uploading shouldn't
1888 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001889
1890 Arguments:
1891 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001892 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001893 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001894
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001895 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001896 """Uploads a change to codereview."""
1897 raise NotImplementedError()
1898
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001899 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001900 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001901
1902 Issue must have been already uploaded and known.
1903 """
1904 raise NotImplementedError()
1905
tandriie113dfd2016-10-11 10:20:12 -07001906 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001907 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriie113dfd2016-10-11 10:20:12 -07001908 raise NotImplementedError()
1909
tandriide281ae2016-10-12 06:02:30 -07001910 def GetIssueOwner(self):
1911 raise NotImplementedError()
1912
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001913 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001914 raise NotImplementedError()
1915
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001916
1917class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001918
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001919 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001920 super(_RietveldChangelistImpl, self).__init__(changelist)
1921 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001922 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001923 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001924
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001925 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001926 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001927 self._props = None
1928 self._rpc_server = None
1929
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001930 def GetCodereviewServer(self):
1931 if not self._rietveld_server:
1932 # If we're on a branch then get the server potentially associated
1933 # with that branch.
1934 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001935 self._rietveld_server = gclient_utils.UpgradeToHttps(
1936 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001937 if not self._rietveld_server:
1938 self._rietveld_server = settings.GetDefaultServerUrl()
1939 return self._rietveld_server
1940
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001941 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001942 """Best effort check that user is authenticated with Rietveld server."""
1943 if self._auth_config.use_oauth2:
1944 authenticator = auth.get_authenticator_for_host(
1945 self.GetCodereviewServer(), self._auth_config)
1946 if not authenticator.has_cached_credentials():
1947 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001948 if refresh:
1949 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001950
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001951 def EnsureCanUploadPatchset(self, force):
1952 # No checks for Rietveld because we are deprecating Rietveld.
1953 pass
1954
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001955 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001956 issue = self.GetIssue()
1957 assert issue
1958 try:
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001959 return self.RpcServer().get_description(issue, force=force).strip()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001960 except urllib2.HTTPError as e:
1961 if e.code == 404:
1962 DieWithError(
1963 ('\nWhile fetching the description for issue %d, received a '
1964 '404 (not found)\n'
1965 'error. It is likely that you deleted this '
1966 'issue on the server. If this is the\n'
1967 'case, please run\n\n'
1968 ' git cl issue 0\n\n'
1969 'to clear the association with the deleted issue. Then run '
1970 'this command again.') % issue)
1971 else:
1972 DieWithError(
1973 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1974 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001975 print('Warning: Failed to retrieve CL description due to network '
1976 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001977 return ''
1978
1979 def GetMostRecentPatchset(self):
1980 return self.GetIssueProperties()['patchsets'][-1]
1981
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001982 def GetIssueProperties(self):
1983 if self._props is None:
1984 issue = self.GetIssue()
1985 if not issue:
1986 self._props = {}
1987 else:
1988 self._props = self.RpcServer().get_issue_properties(issue, True)
1989 return self._props
1990
tandriie113dfd2016-10-11 10:20:12 -07001991 def CannotTriggerTryJobReason(self):
1992 props = self.GetIssueProperties()
1993 if not props:
1994 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1995 if props.get('closed'):
1996 return 'CL %s is closed' % self.GetIssue()
1997 if props.get('private'):
1998 return 'CL %s is private' % self.GetIssue()
1999 return None
2000
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002001 def GetTryJobProperties(self, patchset=None):
2002 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07002003 project = (self.GetIssueProperties() or {}).get('project')
2004 return {
2005 'issue': self.GetIssue(),
2006 'patch_project': project,
2007 'patch_storage': 'rietveld',
2008 'patchset': patchset or self.GetPatchset(),
2009 'rietveld': self.GetCodereviewServer(),
2010 }
2011
tandriide281ae2016-10-12 06:02:30 -07002012 def GetIssueOwner(self):
2013 return (self.GetIssueProperties() or {}).get('owner_email')
2014
Aaron Gable636b13f2017-07-14 10:42:48 -07002015 def AddComment(self, message, publish=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002016 return self.RpcServer().add_comment(self.GetIssue(), message)
2017
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002018 def GetCommentsSummary(self, _readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002019 summary = []
2020 for message in self.GetIssueProperties().get('messages', []):
2021 date = datetime.datetime.strptime(message['date'], '%Y-%m-%d %H:%M:%S.%f')
2022 summary.append(_CommentSummary(
2023 date=date,
2024 disapproval=bool(message['disapproval']),
2025 approval=bool(message['approval']),
2026 sender=message['sender'],
2027 message=message['text'],
2028 ))
2029 return summary
2030
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002031 def GetStatus(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002032 """Applies a rough heuristic to give a simple summary of an issue's review
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002033 or CQ status, assuming adherence to a common workflow.
2034
2035 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gablea1bab272017-04-11 16:38:18 -07002036 * 'error' - error from review tool (including deleted issues)
2037 * 'unsent' - not sent for review
2038 * 'waiting' - waiting for review
2039 * 'reply' - waiting for owner to reply to review
2040 * 'not lgtm' - Code-Review label has been set negatively
2041 * 'lgtm' - LGTM from at least one approved reviewer
2042 * 'commit' - in the commit queue
2043 * 'closed' - closed
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002044 """
2045 if not self.GetIssue():
2046 return None
2047
2048 try:
2049 props = self.GetIssueProperties()
2050 except urllib2.HTTPError:
2051 return 'error'
2052
2053 if props.get('closed'):
2054 # Issue is closed.
2055 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00002056 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002057 # Issue is in the commit queue.
2058 return 'commit'
2059
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002060 messages = props.get('messages') or []
Aaron Gablea1bab272017-04-11 16:38:18 -07002061 if not messages:
2062 # No message was sent.
2063 return 'unsent'
2064
2065 if get_approving_reviewers(props):
2066 return 'lgtm'
2067 elif get_approving_reviewers(props, disapproval=True):
2068 return 'not lgtm'
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002069
tandrii9d2c7a32016-06-22 03:42:45 -07002070 # Skip CQ messages that don't require owner's action.
2071 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
2072 if 'Dry run:' in messages[-1]['text']:
2073 messages.pop()
2074 elif 'The CQ bit was unchecked' in messages[-1]['text']:
2075 # This message always follows prior messages from CQ,
2076 # so skip this too.
2077 messages.pop()
2078 else:
2079 # This is probably a CQ messages warranting user attention.
2080 break
2081
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002082 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07002083 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002084 return 'reply'
2085 return 'waiting'
2086
dsansomee2d6fd92016-09-08 00:10:47 -07002087 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01002088 self.RpcServer().update_description(self.GetIssue(), description)
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002089
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002090 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002091 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002092
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002093 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002094 return self.SetFlags({flag: value})
2095
2096 def SetFlags(self, flags):
2097 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002098 """
phajdan.jr68598232016-08-10 03:28:28 -07002099 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002100 try:
tandrii4b233bd2016-07-06 03:50:29 -07002101 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002102 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002103 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002104 if e.code == 404:
2105 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2106 if e.code == 403:
2107 DieWithError(
2108 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002109 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002110 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002111
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002112 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002113 """Returns an upload.RpcServer() to access this review's rietveld instance.
2114 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002115 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002116 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002117 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01002118 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002119 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002120
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002121 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002122 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002123 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002124
tandrii5d48c322016-08-18 16:19:37 -07002125 @classmethod
2126 def PatchsetConfigKey(cls):
2127 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002128
tandrii5d48c322016-08-18 16:19:37 -07002129 @classmethod
2130 def CodereviewServerConfigKey(cls):
2131 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002132
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002133 def GetRietveldObjForPresubmit(self):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002134 return self.RpcServer()
2135
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002136 def SetCQState(self, new_state):
2137 props = self.GetIssueProperties()
2138 if props.get('private'):
2139 DieWithError('Cannot set-commit on private issue')
2140
2141 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002142 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002143 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002144 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002145 else:
tandrii4b233bd2016-07-06 03:50:29 -07002146 assert new_state == _CQState.DRY_RUN
2147 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002148
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002149 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002150 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002151 # PatchIssue should never be called with a dirty tree. It is up to the
2152 # caller to check this, but just in case we assert here since the
2153 # consequences of the caller not checking this could be dire.
2154 assert(not git_common.is_dirty_git_tree('apply'))
2155 assert(parsed_issue_arg.valid)
2156 self._changelist.issue = parsed_issue_arg.issue
2157 if parsed_issue_arg.hostname:
2158 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2159
skobes6468b902016-10-24 08:45:10 -07002160 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2161 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2162 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002163 try:
skobes6468b902016-10-24 08:45:10 -07002164 scm_obj.apply_patch(patchset_object)
2165 except Exception as e:
2166 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002167 return 1
2168
2169 # If we had an issue, commit the current state and register the issue.
2170 if not nocommit:
Aaron Gabled343c632017-03-15 11:02:26 -07002171 self.SetIssue(self.GetIssue())
2172 self.SetPatchset(patchset)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002173 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2174 'patch from issue %(i)s at patchset '
2175 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2176 % {'i': self.GetIssue(), 'p': patchset})])
vapiera7fbd5a2016-06-16 09:17:49 -07002177 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002178 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002179 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002180 return 0
2181
2182 @staticmethod
2183 def ParseIssueURL(parsed_url):
2184 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2185 return None
wychen3c1c1722016-08-04 11:46:36 -07002186 # Rietveld patch: https://domain/<number>/#ps<patchset>
2187 match = re.match(r'/(\d+)/$', parsed_url.path)
2188 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2189 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002190 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002191 issue=int(match.group(1)),
2192 patchset=int(match2.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002193 hostname=parsed_url.netloc,
2194 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002195 # Typical url: https://domain/<issue_number>[/[other]]
2196 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2197 if match:
skobes6468b902016-10-24 08:45:10 -07002198 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002199 issue=int(match.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002200 hostname=parsed_url.netloc,
2201 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002202 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2203 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2204 if match:
skobes6468b902016-10-24 08:45:10 -07002205 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002206 issue=int(match.group(1)),
2207 patchset=int(match.group(2)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002208 hostname=parsed_url.netloc,
2209 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002210 return None
2211
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002212 def CMDUploadChange(self, options, args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002213 """Upload the patch to Rietveld."""
2214 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2215 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002216 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2217 if options.emulate_svn_auto_props:
2218 upload_args.append('--emulate_svn_auto_props')
2219
2220 change_desc = None
2221
2222 if options.email is not None:
2223 upload_args.extend(['--email', options.email])
2224
2225 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002226 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002227 upload_args.extend(['--title', options.title])
2228 if options.message:
2229 upload_args.extend(['--message', options.message])
2230 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002231 print('This branch is associated with issue %s. '
2232 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002233 else:
nodirca166002016-06-27 10:59:51 -07002234 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002235 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002236 if options.message:
2237 message = options.message
2238 else:
2239 message = CreateDescriptionFromLog(args)
2240 if options.title:
2241 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002242 change_desc = ChangeDescription(message)
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002243 if options.reviewers or options.add_owners_to:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002244 change_desc.update_reviewers(options.reviewers, options.tbrs,
2245 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002246 if not options.force:
Aaron Gable3a16ed12017-03-23 10:51:55 -07002247 change_desc.prompt(bug=options.bug, git_footer=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002248
2249 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002250 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002251 return 1
2252
2253 upload_args.extend(['--message', change_desc.description])
2254 if change_desc.get_reviewers():
2255 upload_args.append('--reviewers=%s' % ','.join(
2256 change_desc.get_reviewers()))
2257 if options.send_mail:
2258 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002259 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002260 upload_args.append('--send_mail')
2261
2262 # We check this before applying rietveld.private assuming that in
2263 # rietveld.cc only addresses which we can send private CLs to are listed
2264 # if rietveld.private is set, and so we should ignore rietveld.cc only
2265 # when --private is specified explicitly on the command line.
2266 if options.private:
2267 logging.warn('rietveld.cc is ignored since private flag is specified. '
2268 'You need to review and add them manually if necessary.')
2269 cc = self.GetCCListWithoutDefault()
2270 else:
2271 cc = self.GetCCList()
2272 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002273 if change_desc.get_cced():
2274 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002275 if cc:
2276 upload_args.extend(['--cc', cc])
2277
2278 if options.private or settings.GetDefaultPrivateFlag() == "True":
2279 upload_args.append('--private')
2280
2281 upload_args.extend(['--git_similarity', str(options.similarity)])
2282 if not options.find_copies:
2283 upload_args.extend(['--git_no_find_copies'])
2284
2285 # Include the upstream repo's URL in the change -- this is useful for
2286 # projects that have their source spread across multiple repos.
2287 remote_url = self.GetGitBaseUrlFromConfig()
2288 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002289 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2290 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2291 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002292 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002293 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002294 target_ref = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002295 if target_ref:
2296 upload_args.extend(['--target_ref', target_ref])
2297
2298 # Look for dependent patchsets. See crbug.com/480453 for more details.
2299 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2300 upstream_branch = ShortBranchName(upstream_branch)
2301 if remote is '.':
2302 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002303 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002304 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002305 print()
2306 print('Skipping dependency patchset upload because git config '
2307 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2308 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002309 else:
2310 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002311 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002312 auth_config=auth_config)
2313 branch_cl_issue_url = branch_cl.GetIssueURL()
2314 branch_cl_issue = branch_cl.GetIssue()
2315 branch_cl_patchset = branch_cl.GetPatchset()
2316 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2317 upload_args.extend(
2318 ['--depends_on_patchset', '%s:%s' % (
2319 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002320 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002321 '\n'
2322 'The current branch (%s) is tracking a local branch (%s) with '
2323 'an associated CL.\n'
2324 'Adding %s/#ps%s as a dependency patchset.\n'
2325 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2326 branch_cl_patchset))
2327
2328 project = settings.GetProject()
2329 if project:
2330 upload_args.extend(['--project', project])
Aaron Gable665a4392017-06-29 10:53:46 -07002331 else:
2332 print()
2333 print('WARNING: Uploading without a project specified. Please ensure '
2334 'your repo\'s codereview.settings has a "PROJECT: foo" line.')
2335 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002336
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002337 try:
2338 upload_args = ['upload'] + upload_args + args
2339 logging.info('upload.RealMain(%s)', upload_args)
2340 issue, patchset = upload.RealMain(upload_args)
2341 issue = int(issue)
2342 patchset = int(patchset)
2343 except KeyboardInterrupt:
2344 sys.exit(1)
2345 except:
2346 # If we got an exception after the user typed a description for their
2347 # change, back up the description before re-raising.
2348 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002349 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002350 raise
2351
2352 if not self.GetIssue():
2353 self.SetIssue(issue)
2354 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002355 return 0
2356
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002357
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002358class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002359 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002360 # auth_config is Rietveld thing, kept here to preserve interface only.
2361 super(_GerritChangelistImpl, self).__init__(changelist)
2362 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002363 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002364 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002365 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002366 # Map from change number (issue) to its detail cache.
2367 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002368
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002369 if codereview_host is not None:
2370 assert not codereview_host.startswith('https://'), codereview_host
2371 self._gerrit_host = codereview_host
2372 self._gerrit_server = 'https://%s' % codereview_host
2373
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002374 def _GetGerritHost(self):
2375 # Lazy load of configs.
2376 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002377 if self._gerrit_host and '.' not in self._gerrit_host:
2378 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2379 # This happens for internal stuff http://crbug.com/614312.
2380 parsed = urlparse.urlparse(self.GetRemoteUrl())
2381 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002382 print('WARNING: using non-https URLs for remote is likely broken\n'
tandriie32e3ea2016-06-22 02:52:48 -07002383 ' Your current remote is: %s' % self.GetRemoteUrl())
2384 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2385 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002386 return self._gerrit_host
2387
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002388 def _GetGitHost(self):
2389 """Returns git host to be used when uploading change to Gerrit."""
2390 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2391
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002392 def GetCodereviewServer(self):
2393 if not self._gerrit_server:
2394 # If we're on a branch then get the server potentially associated
2395 # with that branch.
2396 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002397 self._gerrit_server = self._GitGetBranchConfigValue(
2398 self.CodereviewServerConfigKey())
2399 if self._gerrit_server:
2400 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002401 if not self._gerrit_server:
2402 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2403 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002404 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002405 parts[0] = parts[0] + '-review'
2406 self._gerrit_host = '.'.join(parts)
2407 self._gerrit_server = 'https://%s' % self._gerrit_host
2408 return self._gerrit_server
2409
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002410 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002411 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002412 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002413
tandrii5d48c322016-08-18 16:19:37 -07002414 @classmethod
2415 def PatchsetConfigKey(cls):
2416 return 'gerritpatchset'
2417
2418 @classmethod
2419 def CodereviewServerConfigKey(cls):
2420 return 'gerritserver'
2421
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002422 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002423 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002424 if settings.GetGerritSkipEnsureAuthenticated():
2425 # For projects with unusual authentication schemes.
2426 # See http://crbug.com/603378.
2427 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002428 # Lazy-loader to identify Gerrit and Git hosts.
2429 if gerrit_util.GceAuthenticator.is_gce():
2430 return
2431 self.GetCodereviewServer()
2432 git_host = self._GetGitHost()
2433 assert self._gerrit_server and self._gerrit_host
2434 cookie_auth = gerrit_util.CookiesAuthenticator()
2435
2436 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2437 git_auth = cookie_auth.get_auth_header(git_host)
2438 if gerrit_auth and git_auth:
2439 if gerrit_auth == git_auth:
2440 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002441 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002442 print((
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002443 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002444 ' %s\n'
2445 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002446 ' Consider running the following command:\n'
2447 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002448 ' %s\n'
Andrii Shyshkalov8e4576f2017-05-10 15:46:53 +02002449 ' %s') %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002450 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002451 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002452 cookie_auth.get_new_password_message(git_host)))
2453 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002454 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002455 return
2456 else:
2457 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02002458 ([] if gerrit_auth else [self._gerrit_host]) +
2459 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002460 DieWithError('Credentials for the following hosts are required:\n'
2461 ' %s\n'
2462 'These are read from %s (or legacy %s)\n'
2463 '%s' % (
2464 '\n '.join(missing),
2465 cookie_auth.get_gitcookies_path(),
2466 cookie_auth.get_netrc_path(),
2467 cookie_auth.get_new_password_message(git_host)))
2468
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002469 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002470 if not self.GetIssue():
2471 return
2472
2473 # Warm change details cache now to avoid RPCs later, reducing latency for
2474 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002475 self._GetChangeDetail(
2476 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002477
2478 status = self._GetChangeDetail()['status']
2479 if status in ('MERGED', 'ABANDONED'):
2480 DieWithError('Change %s has been %s, new uploads are not allowed' %
2481 (self.GetIssueURL(),
2482 'submitted' if status == 'MERGED' else 'abandoned'))
2483
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002484 if gerrit_util.GceAuthenticator.is_gce():
2485 return
2486 cookies_user = gerrit_util.CookiesAuthenticator().get_auth_email(
2487 self._GetGerritHost())
2488 if self.GetIssueOwner() == cookies_user:
2489 return
2490 logging.debug('change %s owner is %s, cookies user is %s',
2491 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002492 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002493 # so ask what Gerrit thinks of this user.
2494 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2495 if details['email'] == self.GetIssueOwner():
2496 return
2497 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002498 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002499 'as %s.\n'
2500 'Uploading may fail due to lack of permissions.' %
2501 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2502 confirm_or_exit(action='upload')
2503
2504
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002505 def _PostUnsetIssueProperties(self):
2506 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002507 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002508
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002509 def GetRietveldObjForPresubmit(self):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002510 class ThisIsNotRietveldIssue(object):
2511 def __nonzero__(self):
2512 # This is a hack to make presubmit_support think that rietveld is not
2513 # defined, yet still ensure that calls directly result in a decent
2514 # exception message below.
2515 return False
2516
2517 def __getattr__(self, attr):
2518 print(
2519 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2520 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002521 'Please, either change your PRESUBMIT to not use rietveld_obj.%s,\n'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002522 'or use Rietveld for codereview.\n'
2523 'See also http://crbug.com/579160.' % attr)
2524 raise NotImplementedError()
2525 return ThisIsNotRietveldIssue()
2526
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002527 def GetGerritObjForPresubmit(self):
2528 return presubmit_support.GerritAccessor(self._GetGerritHost())
2529
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002530 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002531 """Apply a rough heuristic to give a simple summary of an issue's review
2532 or CQ status, assuming adherence to a common workflow.
2533
2534 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002535 * 'error' - error from review tool (including deleted issues)
2536 * 'unsent' - no reviewers added
2537 * 'waiting' - waiting for review
2538 * 'reply' - waiting for uploader to reply to review
2539 * 'lgtm' - Code-Review label has been set
2540 * 'commit' - in the commit queue
2541 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002542 """
2543 if not self.GetIssue():
2544 return None
2545
2546 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002547 data = self._GetChangeDetail([
2548 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002549 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002550 return 'error'
2551
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002552 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002553 return 'closed'
2554
Aaron Gable9ab38c62017-04-06 14:36:33 -07002555 if data['labels'].get('Commit-Queue', {}).get('approved'):
2556 # The section will have an "approved" subsection if anyone has voted
2557 # the maximum value on the label.
2558 return 'commit'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002559
Aaron Gable9ab38c62017-04-06 14:36:33 -07002560 if data['labels'].get('Code-Review', {}).get('approved'):
2561 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002562
2563 if not data.get('reviewers', {}).get('REVIEWER', []):
2564 return 'unsent'
2565
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002566 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002567 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2568 last_message_author = messages.pop().get('author', {})
2569 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002570 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2571 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002572 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002573 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002574 if last_message_author.get('_account_id') == owner:
2575 # Most recent message was by owner.
2576 return 'waiting'
2577 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002578 # Some reply from non-owner.
2579 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002580
2581 # Somehow there are no messages even though there are reviewers.
2582 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002583
2584 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002585 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002586 patchset = data['revisions'][data['current_revision']]['_number']
2587 self.SetPatchset(patchset)
2588 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002589
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002590 def FetchDescription(self, force=False):
2591 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2592 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002593 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002594 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002595
dsansomee2d6fd92016-09-08 00:10:47 -07002596 def UpdateDescriptionRemote(self, description, force=False):
2597 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2598 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002599 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002600 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002601 'unpublished edit. Either publish the edit in the Gerrit web UI '
2602 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002603
2604 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2605 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002606 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002607 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002608
Aaron Gable636b13f2017-07-14 10:42:48 -07002609 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002610 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
Aaron Gable636b13f2017-07-14 10:42:48 -07002611 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002612
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002613 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002614 # DETAILED_ACCOUNTS is to get emails in accounts.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002615 messages = self._GetChangeDetail(
2616 options=['MESSAGES', 'DETAILED_ACCOUNTS']).get('messages', [])
2617 file_comments = gerrit_util.GetChangeComments(
2618 self._GetGerritHost(), self.GetIssue())
2619
2620 # Build dictionary of file comments for easy access and sorting later.
2621 # {author+date: {path: {patchset: {line: url+message}}}}
2622 comments = collections.defaultdict(
2623 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2624 for path, line_comments in file_comments.iteritems():
2625 for comment in line_comments:
2626 if comment.get('tag', '').startswith('autogenerated'):
2627 continue
2628 key = (comment['author']['email'], comment['updated'])
2629 if comment.get('side', 'REVISION') == 'PARENT':
2630 patchset = 'Base'
2631 else:
2632 patchset = 'PS%d' % comment['patch_set']
2633 line = comment.get('line', 0)
2634 url = ('https://%s/c/%s/%s/%s#%s%s' %
2635 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2636 'b' if comment.get('side') == 'PARENT' else '',
2637 str(line) if line else ''))
2638 comments[key][path][patchset][line] = (url, comment['message'])
2639
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002640 summary = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002641 for msg in messages:
2642 # Don't bother showing autogenerated messages.
2643 if msg.get('tag') and msg.get('tag').startswith('autogenerated'):
2644 continue
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002645 # Gerrit spits out nanoseconds.
2646 assert len(msg['date'].split('.')[-1]) == 9
2647 date = datetime.datetime.strptime(msg['date'][:-3],
2648 '%Y-%m-%d %H:%M:%S.%f')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002649 message = msg['message']
2650 key = (msg['author']['email'], msg['date'])
2651 if key in comments:
2652 message += '\n'
2653 for path, patchsets in sorted(comments.get(key, {}).items()):
2654 if readable:
2655 message += '\n%s' % path
2656 for patchset, lines in sorted(patchsets.items()):
2657 for line, (url, content) in sorted(lines.items()):
2658 if line:
2659 line_str = 'Line %d' % line
2660 path_str = '%s:%d:' % (path, line)
2661 else:
2662 line_str = 'File comment'
2663 path_str = '%s:0:' % path
2664 if readable:
2665 message += '\n %s, %s: %s' % (patchset, line_str, url)
2666 message += '\n %s\n' % content
2667 else:
2668 message += '\n%s ' % path_str
2669 message += '\n%s\n' % content
2670
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002671 summary.append(_CommentSummary(
2672 date=date,
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002673 message=message,
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002674 sender=msg['author']['email'],
2675 # These could be inferred from the text messages and correlated with
2676 # Code-Review label maximum, however this is not reliable.
2677 # Leaving as is until the need arises.
2678 approval=False,
2679 disapproval=False,
2680 ))
2681 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002682
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002683 def CloseIssue(self):
2684 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2685
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002686 def SubmitIssue(self, wait_for_merge=True):
2687 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2688 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002689
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002690 def _GetChangeDetail(self, options=None, issue=None,
2691 no_cache=False):
2692 """Returns details of the issue by querying Gerrit and caching results.
2693
2694 If fresh data is needed, set no_cache=True which will clear cache and
2695 thus new data will be fetched from Gerrit.
2696 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002697 options = options or []
2698 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002699 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002700
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002701 # Optimization to avoid multiple RPCs:
2702 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2703 'CURRENT_COMMIT' not in options):
2704 options.append('CURRENT_COMMIT')
2705
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002706 # Normalize issue and options for consistent keys in cache.
2707 issue = str(issue)
2708 options = [o.upper() for o in options]
2709
2710 # Check in cache first unless no_cache is True.
2711 if no_cache:
2712 self._detail_cache.pop(issue, None)
2713 else:
2714 options_set = frozenset(options)
2715 for cached_options_set, data in self._detail_cache.get(issue, []):
2716 # Assumption: data fetched before with extra options is suitable
2717 # for return for a smaller set of options.
2718 # For example, if we cached data for
2719 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2720 # and request is for options=[CURRENT_REVISION],
2721 # THEN we can return prior cached data.
2722 if options_set.issubset(cached_options_set):
2723 return data
2724
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002725 try:
Aaron Gable19ee16c2017-04-18 11:56:35 -07002726 data = gerrit_util.GetChangeDetail(
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002727 self._GetGerritHost(), str(issue), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002728 except gerrit_util.GerritError as e:
2729 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002730 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002731 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002732
2733 self._detail_cache.setdefault(issue, []).append((frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002734 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002735
agable32978d92016-11-01 12:55:02 -07002736 def _GetChangeCommit(self, issue=None):
2737 issue = issue or self.GetIssue()
2738 assert issue, 'issue is required to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002739 try:
2740 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2741 except gerrit_util.GerritError as e:
2742 if e.http_status == 404:
2743 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
2744 raise
agable32978d92016-11-01 12:55:02 -07002745 return data
2746
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002747 def CMDLand(self, force, bypass_hooks, verbose):
2748 if git_common.is_dirty_git_tree('land'):
2749 return 1
tandriid60367b2016-06-22 05:25:12 -07002750 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2751 if u'Commit-Queue' in detail.get('labels', {}):
2752 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002753 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2754 'which can test and land changes for you. '
2755 'Are you sure you wish to bypass it?\n',
2756 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002757
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002758 differs = True
tandriic4344b52016-08-29 06:04:54 -07002759 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002760 # Note: git diff outputs nothing if there is no diff.
2761 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002762 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002763 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002764 if detail['current_revision'] == last_upload:
2765 differs = False
2766 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002767 print('WARNING: Local branch contents differ from latest uploaded '
2768 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002769 if differs:
2770 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002771 confirm_or_exit(
2772 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2773 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002774 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002775 elif not bypass_hooks:
2776 hook_results = self.RunHook(
2777 committing=True,
2778 may_prompt=not force,
2779 verbose=verbose,
2780 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2781 if not hook_results.should_continue():
2782 return 1
2783
2784 self.SubmitIssue(wait_for_merge=True)
2785 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002786 links = self._GetChangeCommit().get('web_links', [])
2787 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002788 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002789 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002790 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002791 return 0
2792
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002793 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002794 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002795 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002796 assert not directory
2797 assert parsed_issue_arg.valid
2798
2799 self._changelist.issue = parsed_issue_arg.issue
2800
2801 if parsed_issue_arg.hostname:
2802 self._gerrit_host = parsed_issue_arg.hostname
2803 self._gerrit_server = 'https://%s' % self._gerrit_host
2804
tandriic2405f52016-10-10 08:13:15 -07002805 try:
2806 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002807 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002808 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002809
2810 if not parsed_issue_arg.patchset:
2811 # Use current revision by default.
2812 revision_info = detail['revisions'][detail['current_revision']]
2813 patchset = int(revision_info['_number'])
2814 else:
2815 patchset = parsed_issue_arg.patchset
2816 for revision_info in detail['revisions'].itervalues():
2817 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2818 break
2819 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002820 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002821 (parsed_issue_arg.patchset, self.GetIssue()))
2822
Aaron Gable697a91b2018-01-19 15:20:15 -08002823 remote_url = self._changelist.GetRemoteUrl()
2824 if remote_url.endswith('.git'):
2825 remote_url = remote_url[:-len('.git')]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002826 fetch_info = revision_info['fetch']['http']
Aaron Gable697a91b2018-01-19 15:20:15 -08002827
2828 if remote_url != fetch_info['url']:
2829 DieWithError('Trying to patch a change from %s but this repo appears '
2830 'to be %s.' % (fetch_info['url'], remote_url))
2831
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002832 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002833
Aaron Gable62619a32017-06-16 08:22:09 -07002834 if force:
2835 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2836 print('Checked out commit for change %i patchset %i locally' %
2837 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002838 elif nocommit:
2839 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2840 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002841 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002842 RunGit(['cherry-pick', 'FETCH_HEAD'])
2843 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002844 (parsed_issue_arg.issue, patchset))
2845 print('Note: this created a local commit which does not have '
2846 'the same hash as the one uploaded for review. This will make '
2847 'uploading changes based on top of this branch difficult.\n'
2848 'If you want to do that, use "git cl patch --force" instead.')
2849
Stefan Zagerd08043c2017-10-12 12:07:02 -07002850 if self.GetBranch():
2851 self.SetIssue(parsed_issue_arg.issue)
2852 self.SetPatchset(patchset)
2853 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2854 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2855 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2856 else:
2857 print('WARNING: You are in detached HEAD state.\n'
2858 'The patch has been applied to your checkout, but you will not be '
2859 'able to upload a new patch set to the gerrit issue.\n'
2860 'Try using the \'-b\' option if you would like to work on a '
2861 'branch and/or upload a new patch set.')
2862
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002863 return 0
2864
2865 @staticmethod
2866 def ParseIssueURL(parsed_url):
2867 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2868 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002869 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2870 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002871 # Short urls like https://domain/<issue_number> can be used, but don't allow
2872 # specifying the patchset (you'd 404), but we allow that here.
2873 if parsed_url.path == '/':
2874 part = parsed_url.fragment
2875 else:
2876 part = parsed_url.path
Aaron Gable01b91062017-08-24 17:48:40 -07002877 match = re.match('(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002878 if match:
2879 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002880 issue=int(match.group(3)),
2881 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002882 hostname=parsed_url.netloc,
2883 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002884 return None
2885
tandrii16e0b4e2016-06-07 10:34:28 -07002886 def _GerritCommitMsgHookCheck(self, offer_removal):
2887 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2888 if not os.path.exists(hook):
2889 return
2890 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2891 # custom developer made one.
2892 data = gclient_utils.FileRead(hook)
2893 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2894 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002895 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002896 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002897 'and may interfere with it in subtle ways.\n'
2898 'We recommend you remove the commit-msg hook.')
2899 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002900 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002901 gclient_utils.rm_file_or_tree(hook)
2902 print('Gerrit commit-msg hook removed.')
2903 else:
2904 print('OK, will keep Gerrit commit-msg hook in place.')
2905
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002906 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002907 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002908 if options.squash and options.no_squash:
2909 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002910
2911 if not options.squash and not options.no_squash:
2912 # Load default for user, repo, squash=true, in this order.
2913 options.squash = settings.GetSquashGerritUploads()
2914 elif options.no_squash:
2915 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002916
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002917 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002918 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002919
Aaron Gableb56ad332017-01-06 15:24:31 -08002920 # This may be None; default fallback value is determined in logic below.
2921 title = options.title
2922
Dominic Battre7d1c4842017-10-27 09:17:28 +02002923 # Extract bug number from branch name.
2924 bug = options.bug
2925 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2926 if not bug and match:
2927 bug = match.group(1)
2928
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002929 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002930 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002931 if self.GetIssue():
2932 # Try to get the message from a previous upload.
2933 message = self.GetDescription()
2934 if not message:
2935 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002936 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002937 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002938 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002939 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002940 # When uploading a subsequent patchset, -m|--message is taken
2941 # as the patchset title if --title was not provided.
2942 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002943 else:
2944 default_title = RunGit(
2945 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002946 if options.force:
2947 title = default_title
2948 else:
2949 title = ask_for_data(
2950 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002951 change_id = self._GetChangeDetail()['change_id']
2952 while True:
2953 footer_change_ids = git_footers.get_footer_change_id(message)
2954 if footer_change_ids == [change_id]:
2955 break
2956 if not footer_change_ids:
2957 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002958 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002959 continue
2960 # There is already a valid footer but with different or several ids.
2961 # Doing this automatically is non-trivial as we don't want to lose
2962 # existing other footers, yet we want to append just 1 desired
2963 # Change-Id. Thus, just create a new footer, but let user verify the
2964 # new description.
2965 message = '%s\n\nChange-Id: %s' % (message, change_id)
2966 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002967 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002968 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002969 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002970 'Please, check the proposed correction to the description, '
2971 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2972 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2973 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002974 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002975 if not options.force:
2976 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002977 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002978 message = change_desc.description
2979 if not message:
2980 DieWithError("Description is empty. Aborting...")
2981 # Continue the while loop.
2982 # Sanity check of this code - we should end up with proper message
2983 # footer.
2984 assert [change_id] == git_footers.get_footer_change_id(message)
2985 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002986 else: # if not self.GetIssue()
2987 if options.message:
2988 message = options.message
2989 else:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002990 message = CreateDescriptionFromLog(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002991 if options.title:
2992 message = options.title + '\n\n' + message
2993 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002994
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002995 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02002996 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002997 # On first upload, patchset title is always this string, while
2998 # --title flag gets converted to first line of message.
2999 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003000 if not change_desc.description:
3001 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02003002 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003003 if len(change_ids) > 1:
3004 DieWithError('too many Change-Id footers, at most 1 allowed.')
3005 if not change_ids:
3006 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02003007 change_desc.set_description(git_footers.add_footer_change_id(
3008 change_desc.description,
3009 GenerateGerritChangeId(change_desc.description)))
3010 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003011 assert len(change_ids) == 1
3012 change_id = change_ids[0]
3013
Robert Iannuccidb02dd02017-04-19 12:18:20 -07003014 if options.reviewers or options.tbrs or options.add_owners_to:
3015 change_desc.update_reviewers(options.reviewers, options.tbrs,
3016 options.add_owners_to, change)
3017
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003018 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003019 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
3020 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003021 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07003022 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
3023 desc_tempfile.write(change_desc.description)
3024 desc_tempfile.close()
3025 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
3026 '-F', desc_tempfile.name]).strip()
3027 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003028 else:
3029 change_desc = ChangeDescription(
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003030 options.message or CreateDescriptionFromLog(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003031 if not change_desc.description:
3032 DieWithError("Description is empty. Aborting...")
3033
3034 if not git_footers.get_footer_change_id(change_desc.description):
3035 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003036 change_desc.set_description(
3037 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07003038 if options.reviewers or options.tbrs or options.add_owners_to:
3039 change_desc.update_reviewers(options.reviewers, options.tbrs,
3040 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003041 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003042 # For no-squash mode, we assume the remote called "origin" is the one we
3043 # want. It is not worthwhile to support different workflows for
3044 # no-squash mode.
3045 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003046 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
3047
3048 assert change_desc
3049 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
3050 ref_to_push)]).splitlines()
3051 if len(commits) > 1:
3052 print('WARNING: This will upload %d commits. Run the following command '
3053 'to see which commits will be uploaded: ' % len(commits))
3054 print('git log %s..%s' % (parent, ref_to_push))
3055 print('You can also use `git squash-branch` to squash these into a '
3056 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003057 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003058
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003059 if options.reviewers or options.tbrs or options.add_owners_to:
3060 change_desc.update_reviewers(options.reviewers, options.tbrs,
3061 options.add_owners_to, change)
3062
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00003063 # Extra options that can be specified at push time. Doc:
3064 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003065 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003066
Aaron Gable844cf292017-06-28 11:32:59 -07003067 # By default, new changes are started in WIP mode, and subsequent patchsets
3068 # don't send email. At any time, passing --send-mail will mark the change
3069 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07003070 if options.send_mail:
3071 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07003072 refspec_opts.append('notify=ALL')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003073 elif not self.GetIssue():
3074 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07003075 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003076 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07003077
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003078 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07003079 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003080
Aaron Gable9b713dd2016-12-14 16:04:21 -08003081 if title:
Nick Carter8692b182017-11-06 16:30:38 -08003082 # Punctuation and whitespace in |title| must be percent-encoded.
3083 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00003084
agablec6787972016-09-09 16:13:34 -07003085 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07003086 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07003087
rmistry9eadede2016-09-19 11:22:43 -07003088 if options.topic:
3089 # Documentation on Gerrit topics is here:
3090 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003091 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07003092
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003093 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08003094 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003095 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08003096 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003097 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
3098
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003099 refspec_suffix = ''
3100 if refspec_opts:
3101 refspec_suffix = '%' + ','.join(refspec_opts)
3102 assert ' ' not in refspec_suffix, (
3103 'spaces not allowed in refspec: "%s"' % refspec_suffix)
3104 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
3105
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003106 try:
3107 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003108 ['git', 'push', self.GetRemoteUrl(), refspec],
3109 print_stdout=True,
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003110 # Flush after every line: useful for seeing progress when running as
3111 # recipe.
3112 filter_fn=lambda _: sys.stdout.flush())
3113 except subprocess2.CalledProcessError:
3114 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003115 'for the reason of the failure.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003116 'Hint: run command below to diagnose common Git/Gerrit '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003117 'credential problems:\n'
3118 ' git cl creds-check\n',
3119 change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003120
3121 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07003122 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003123 change_numbers = [m.group(1)
3124 for m in map(regex.match, push_stdout.splitlines())
3125 if m]
3126 if len(change_numbers) != 1:
3127 DieWithError(
3128 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11003129 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003130 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07003131 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07003132
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003133 reviewers = sorted(change_desc.get_reviewers())
3134
tandrii88189772016-09-29 04:29:57 -07003135 # Add cc's from the CC_LIST and --cc flag (if any).
Aaron Gabled1052492017-05-15 15:05:34 -07003136 if not options.private:
3137 cc = self.GetCCList().split(',')
3138 else:
3139 cc = []
tandrii88189772016-09-29 04:29:57 -07003140 if options.cc:
3141 cc.extend(options.cc)
3142 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07003143 if change_desc.get_cced():
3144 cc.extend(change_desc.get_cced())
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003145
3146 gerrit_util.AddReviewers(
3147 self._GetGerritHost(), self.GetIssue(), reviewers, cc,
3148 notify=bool(options.send_mail))
3149
Aaron Gablefd238082017-06-07 13:42:34 -07003150 if change_desc.get_reviewers(tbr_only=True):
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003151 labels = self._GetChangeDetail(['LABELS']).get('labels', {})
3152 score = 1
3153 if 'Code-Review' in labels and 'values' in labels['Code-Review']:
3154 score = max([int(x) for x in labels['Code-Review']['values'].keys()])
3155 print('Adding self-LGTM (Code-Review +%d) because of TBRs.' % score)
Aaron Gablefd238082017-06-07 13:42:34 -07003156 gerrit_util.SetReview(
3157 self._GetGerritHost(), self.GetIssue(),
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003158 msg='Self-approving for TBR',
3159 labels={'Code-Review': score})
Aaron Gablefd238082017-06-07 13:42:34 -07003160
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003161 return 0
3162
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003163 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
3164 change_desc):
3165 """Computes parent of the generated commit to be uploaded to Gerrit.
3166
3167 Returns revision or a ref name.
3168 """
3169 if custom_cl_base:
3170 # Try to avoid creating additional unintended CLs when uploading, unless
3171 # user wants to take this risk.
3172 local_ref_of_target_remote = self.GetRemoteBranch()[1]
3173 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
3174 local_ref_of_target_remote])
3175 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003176 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003177 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
3178 'If you proceed with upload, more than 1 CL may be created by '
3179 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
3180 'If you are certain that specified base `%s` has already been '
3181 'uploaded to Gerrit as another CL, you may proceed.\n' %
3182 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
3183 if not force:
3184 confirm_or_exit(
3185 'Do you take responsibility for cleaning up potential mess '
3186 'resulting from proceeding with upload?',
3187 action='upload')
3188 return custom_cl_base
3189
Aaron Gablef97e33d2017-03-30 15:44:27 -07003190 if remote != '.':
3191 return self.GetCommonAncestorWithUpstream()
3192
3193 # If our upstream branch is local, we base our squashed commit on its
3194 # squashed version.
3195 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
3196
Aaron Gablef97e33d2017-03-30 15:44:27 -07003197 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07003198 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003199
3200 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003201 # TODO(tandrii): consider checking parent change in Gerrit and using its
3202 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
3203 # the tree hash of the parent branch. The upside is less likely bogus
3204 # requests to reupload parent change just because it's uploadhash is
3205 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07003206 parent = RunGit(['config',
3207 'branch.%s.gerritsquashhash' % upstream_branch_name],
3208 error_ok=True).strip()
3209 # Verify that the upstream branch has been uploaded too, otherwise
3210 # Gerrit will create additional CLs when uploading.
3211 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
3212 RunGitSilent(['rev-parse', parent + ':'])):
3213 DieWithError(
3214 '\nUpload upstream branch %s first.\n'
3215 'It is likely that this branch has been rebased since its last '
3216 'upload, so you just need to upload it again.\n'
3217 '(If you uploaded it with --no-squash, then branch dependencies '
3218 'are not supported, and you should reupload with --squash.)'
3219 % upstream_branch_name,
3220 change_desc)
3221 return parent
3222
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003223 def _AddChangeIdToCommitMessage(self, options, args):
3224 """Re-commits using the current message, assumes the commit hook is in
3225 place.
3226 """
3227 log_desc = options.message or CreateDescriptionFromLog(args)
3228 git_command = ['commit', '--amend', '-m', log_desc]
3229 RunGit(git_command)
3230 new_log_desc = CreateDescriptionFromLog(args)
3231 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003232 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003233 return new_log_desc
3234 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00003235 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003236
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003237 def SetCQState(self, new_state):
3238 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003239 vote_map = {
3240 _CQState.NONE: 0,
3241 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003242 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003243 }
Aaron Gablefc62f762017-07-17 11:12:07 -07003244 labels = {'Commit-Queue': vote_map[new_state]}
3245 notify = False if new_state == _CQState.DRY_RUN else None
3246 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
3247 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003248
tandriie113dfd2016-10-11 10:20:12 -07003249 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003250 try:
3251 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003252 except GerritChangeNotExists:
3253 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003254
3255 if data['status'] in ('ABANDONED', 'MERGED'):
3256 return 'CL %s is closed' % self.GetIssue()
3257
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003258 def GetTryJobProperties(self, patchset=None):
3259 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07003260 data = self._GetChangeDetail(['ALL_REVISIONS'])
3261 patchset = int(patchset or self.GetPatchset())
3262 assert patchset
3263 revision_data = None # Pylint wants it to be defined.
3264 for revision_data in data['revisions'].itervalues():
3265 if int(revision_data['_number']) == patchset:
3266 break
3267 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003268 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003269 (patchset, self.GetIssue()))
3270 return {
3271 'patch_issue': self.GetIssue(),
3272 'patch_set': patchset or self.GetPatchset(),
3273 'patch_project': data['project'],
3274 'patch_storage': 'gerrit',
3275 'patch_ref': revision_data['fetch']['http']['ref'],
3276 'patch_repository_url': revision_data['fetch']['http']['url'],
3277 'patch_gerrit_url': self.GetCodereviewServer(),
3278 }
tandriie113dfd2016-10-11 10:20:12 -07003279
tandriide281ae2016-10-12 06:02:30 -07003280 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003281 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003282
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003283
3284_CODEREVIEW_IMPLEMENTATIONS = {
3285 'rietveld': _RietveldChangelistImpl,
3286 'gerrit': _GerritChangelistImpl,
3287}
3288
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003289
iannuccie53c9352016-08-17 14:40:40 -07003290def _add_codereview_issue_select_options(parser, extra=""):
3291 _add_codereview_select_options(parser)
3292
3293 text = ('Operate on this issue number instead of the current branch\'s '
3294 'implicit issue.')
3295 if extra:
3296 text += ' '+extra
3297 parser.add_option('-i', '--issue', type=int, help=text)
3298
3299
3300def _process_codereview_issue_select_options(parser, options):
3301 _process_codereview_select_options(parser, options)
3302 if options.issue is not None and not options.forced_codereview:
3303 parser.error('--issue must be specified with either --rietveld or --gerrit')
3304
3305
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003306def _add_codereview_select_options(parser):
3307 """Appends --gerrit and --rietveld options to force specific codereview."""
3308 parser.codereview_group = optparse.OptionGroup(
3309 parser, 'EXPERIMENTAL! Codereview override options')
3310 parser.add_option_group(parser.codereview_group)
3311 parser.codereview_group.add_option(
3312 '--gerrit', action='store_true',
3313 help='Force the use of Gerrit for codereview')
3314 parser.codereview_group.add_option(
3315 '--rietveld', action='store_true',
3316 help='Force the use of Rietveld for codereview')
3317
3318
3319def _process_codereview_select_options(parser, options):
3320 if options.gerrit and options.rietveld:
3321 parser.error('Options --gerrit and --rietveld are mutually exclusive')
3322 options.forced_codereview = None
3323 if options.gerrit:
3324 options.forced_codereview = 'gerrit'
3325 elif options.rietveld:
3326 options.forced_codereview = 'rietveld'
3327
3328
tandriif9aefb72016-07-01 09:06:51 -07003329def _get_bug_line_values(default_project, bugs):
3330 """Given default_project and comma separated list of bugs, yields bug line
3331 values.
3332
3333 Each bug can be either:
3334 * a number, which is combined with default_project
3335 * string, which is left as is.
3336
3337 This function may produce more than one line, because bugdroid expects one
3338 project per line.
3339
3340 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3341 ['v8:123', 'chromium:789']
3342 """
3343 default_bugs = []
3344 others = []
3345 for bug in bugs.split(','):
3346 bug = bug.strip()
3347 if bug:
3348 try:
3349 default_bugs.append(int(bug))
3350 except ValueError:
3351 others.append(bug)
3352
3353 if default_bugs:
3354 default_bugs = ','.join(map(str, default_bugs))
3355 if default_project:
3356 yield '%s:%s' % (default_project, default_bugs)
3357 else:
3358 yield default_bugs
3359 for other in sorted(others):
3360 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3361 yield other
3362
3363
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003364class ChangeDescription(object):
3365 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003366 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003367 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003368 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003369 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003370 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3371 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
3372 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
3373 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003374
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003375 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003376 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003377
agable@chromium.org42c20792013-09-12 17:34:49 +00003378 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003379 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003380 return '\n'.join(self._description_lines)
3381
3382 def set_description(self, desc):
3383 if isinstance(desc, basestring):
3384 lines = desc.splitlines()
3385 else:
3386 lines = [line.rstrip() for line in desc]
3387 while lines and not lines[0]:
3388 lines.pop(0)
3389 while lines and not lines[-1]:
3390 lines.pop(-1)
3391 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003392
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003393 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3394 """Rewrites the R=/TBR= line(s) as a single line each.
3395
3396 Args:
3397 reviewers (list(str)) - list of additional emails to use for reviewers.
3398 tbrs (list(str)) - list of additional emails to use for TBRs.
3399 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3400 the change that are missing OWNER coverage. If this is not None, you
3401 must also pass a value for `change`.
3402 change (Change) - The Change that should be used for OWNERS lookups.
3403 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003404 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003405 assert isinstance(tbrs, list), tbrs
3406
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003407 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003408 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003409
3410 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003411 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003412
3413 reviewers = set(reviewers)
3414 tbrs = set(tbrs)
3415 LOOKUP = {
3416 'TBR': tbrs,
3417 'R': reviewers,
3418 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003419
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003420 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003421 regexp = re.compile(self.R_LINE)
3422 matches = [regexp.match(line) for line in self._description_lines]
3423 new_desc = [l for i, l in enumerate(self._description_lines)
3424 if not matches[i]]
3425 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003426
agable@chromium.org42c20792013-09-12 17:34:49 +00003427 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003428
3429 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003430 for match in matches:
3431 if not match:
3432 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003433 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3434
3435 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003436 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003437 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003438 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003439 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003440 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003441 LOOKUP[add_owners_to].update(
3442 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003443
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003444 # If any folks ended up in both groups, remove them from tbrs.
3445 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003446
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003447 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3448 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003449
3450 # Put the new lines in the description where the old first R= line was.
3451 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3452 if 0 <= line_loc < len(self._description_lines):
3453 if new_tbr_line:
3454 self._description_lines.insert(line_loc, new_tbr_line)
3455 if new_r_line:
3456 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003457 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003458 if new_r_line:
3459 self.append_footer(new_r_line)
3460 if new_tbr_line:
3461 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003462
Aaron Gable3a16ed12017-03-23 10:51:55 -07003463 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003464 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003465 self.set_description([
3466 '# Enter a description of the change.',
3467 '# This will be displayed on the codereview site.',
3468 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003469 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003470 '--------------------',
3471 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003472
agable@chromium.org42c20792013-09-12 17:34:49 +00003473 regexp = re.compile(self.BUG_LINE)
3474 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003475 prefix = settings.GetBugPrefix()
3476 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003477 if git_footer:
3478 self.append_footer('Bug: %s' % ', '.join(values))
3479 else:
3480 for value in values:
3481 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003482
agable@chromium.org42c20792013-09-12 17:34:49 +00003483 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003484 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003485 if not content:
3486 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003487 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003488
Bruce Dawson2377b012018-01-11 16:46:49 -08003489 # Strip off comments and default inserted "Bug:" line.
3490 clean_lines = [line.rstrip() for line in lines if not
3491 (line.startswith('#') or line.rstrip() == "Bug:")]
agable@chromium.org42c20792013-09-12 17:34:49 +00003492 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003493 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003494 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003495
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003496 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003497 """Adds a footer line to the description.
3498
3499 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3500 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3501 that Gerrit footers are always at the end.
3502 """
3503 parsed_footer_line = git_footers.parse_footer(line)
3504 if parsed_footer_line:
3505 # Line is a gerrit footer in the form: Footer-Key: any value.
3506 # Thus, must be appended observing Gerrit footer rules.
3507 self.set_description(
3508 git_footers.add_footer(self.description,
3509 key=parsed_footer_line[0],
3510 value=parsed_footer_line[1]))
3511 return
3512
3513 if not self._description_lines:
3514 self._description_lines.append(line)
3515 return
3516
3517 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3518 if gerrit_footers:
3519 # git_footers.split_footers ensures that there is an empty line before
3520 # actual (gerrit) footers, if any. We have to keep it that way.
3521 assert top_lines and top_lines[-1] == ''
3522 top_lines, separator = top_lines[:-1], top_lines[-1:]
3523 else:
3524 separator = [] # No need for separator if there are no gerrit_footers.
3525
3526 prev_line = top_lines[-1] if top_lines else ''
3527 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3528 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3529 top_lines.append('')
3530 top_lines.append(line)
3531 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003532
tandrii99a72f22016-08-17 14:33:24 -07003533 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003534 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003535 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003536 reviewers = [match.group(2).strip()
3537 for match in matches
3538 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003539 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003540
bradnelsond975b302016-10-23 12:20:23 -07003541 def get_cced(self):
3542 """Retrieves the list of reviewers."""
3543 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3544 cced = [match.group(2).strip() for match in matches if match]
3545 return cleanup_list(cced)
3546
Nodir Turakulov23b82142017-11-16 11:04:25 -08003547 def get_hash_tags(self):
3548 """Extracts and sanitizes a list of Gerrit hashtags."""
3549 subject = (self._description_lines or ('',))[0]
3550 subject = re.sub(
3551 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3552
3553 tags = []
3554 start = 0
3555 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3556 while True:
3557 m = bracket_exp.match(subject, start)
3558 if not m:
3559 break
3560 tags.append(self.sanitize_hash_tag(m.group(1)))
3561 start = m.end()
3562
3563 if not tags:
3564 # Try "Tag: " prefix.
3565 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3566 if m:
3567 tags.append(self.sanitize_hash_tag(m.group(1)))
3568 return tags
3569
3570 @classmethod
3571 def sanitize_hash_tag(cls, tag):
3572 """Returns a sanitized Gerrit hash tag.
3573
3574 A sanitized hashtag can be used as a git push refspec parameter value.
3575 """
3576 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3577
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003578 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3579 """Updates this commit description given the parent.
3580
3581 This is essentially what Gnumbd used to do.
3582 Consult https://goo.gl/WMmpDe for more details.
3583 """
3584 assert parent_msg # No, orphan branch creation isn't supported.
3585 assert parent_hash
3586 assert dest_ref
3587 parent_footer_map = git_footers.parse_footers(parent_msg)
3588 # This will also happily parse svn-position, which GnumbD is no longer
3589 # supporting. While we'd generate correct footers, the verifier plugin
3590 # installed in Gerrit will block such commit (ie git push below will fail).
3591 parent_position = git_footers.get_position(parent_footer_map)
3592
3593 # Cherry-picks may have last line obscuring their prior footers,
3594 # from git_footers perspective. This is also what Gnumbd did.
3595 cp_line = None
3596 if (self._description_lines and
3597 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3598 cp_line = self._description_lines.pop()
3599
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003600 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003601
3602 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3603 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003604 for i, line in enumerate(footer_lines):
3605 k, v = git_footers.parse_footer(line) or (None, None)
3606 if k and k.startswith('Cr-'):
3607 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003608
3609 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003610 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003611 if parent_position[0] == dest_ref:
3612 # Same branch as parent.
3613 number = int(parent_position[1]) + 1
3614 else:
3615 number = 1 # New branch, and extra lineage.
3616 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3617 int(parent_position[1])))
3618
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003619 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3620 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003621
3622 self._description_lines = top_lines
3623 if cp_line:
3624 self._description_lines.append(cp_line)
3625 if self._description_lines[-1] != '':
3626 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003627 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003628
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003629
Aaron Gablea1bab272017-04-11 16:38:18 -07003630def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003631 """Retrieves the reviewers that approved a CL from the issue properties with
3632 messages.
3633
3634 Note that the list may contain reviewers that are not committer, thus are not
3635 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003636
3637 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003638 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003639 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003640 return sorted(
3641 set(
3642 message['sender']
3643 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003644 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003645 )
3646 )
3647
3648
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003649def FindCodereviewSettingsFile(filename='codereview.settings'):
3650 """Finds the given file starting in the cwd and going up.
3651
3652 Only looks up to the top of the repository unless an
3653 'inherit-review-settings-ok' file exists in the root of the repository.
3654 """
3655 inherit_ok_file = 'inherit-review-settings-ok'
3656 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003657 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003658 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3659 root = '/'
3660 while True:
3661 if filename in os.listdir(cwd):
3662 if os.path.isfile(os.path.join(cwd, filename)):
3663 return open(os.path.join(cwd, filename))
3664 if cwd == root:
3665 break
3666 cwd = os.path.dirname(cwd)
3667
3668
3669def LoadCodereviewSettingsFromFile(fileobj):
3670 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003671 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003672
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003673 def SetProperty(name, setting, unset_error_ok=False):
3674 fullname = 'rietveld.' + name
3675 if setting in keyvals:
3676 RunGit(['config', fullname, keyvals[setting]])
3677 else:
3678 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3679
tandrii48df5812016-10-17 03:55:37 -07003680 if not keyvals.get('GERRIT_HOST', False):
3681 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003682 # Only server setting is required. Other settings can be absent.
3683 # In that case, we ignore errors raised during option deletion attempt.
3684 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003685 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003686 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3687 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003688 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003689 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3690 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003691 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003692 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3693 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003694
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003695 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003696 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003697
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003698 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003699 RunGit(['config', 'gerrit.squash-uploads',
3700 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003701
tandrii@chromium.org28253532016-04-14 13:46:56 +00003702 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003703 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003704 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3705
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003706 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003707 # should be of the form
3708 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3709 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003710 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3711 keyvals['ORIGIN_URL_CONFIG']])
3712
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003713
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003714def urlretrieve(source, destination):
3715 """urllib is broken for SSL connections via a proxy therefore we
3716 can't use urllib.urlretrieve()."""
3717 with open(destination, 'w') as f:
3718 f.write(urllib2.urlopen(source).read())
3719
3720
ukai@chromium.org712d6102013-11-27 00:52:58 +00003721def hasSheBang(fname):
3722 """Checks fname is a #! script."""
3723 with open(fname) as f:
3724 return f.read(2).startswith('#!')
3725
3726
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003727# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3728def DownloadHooks(*args, **kwargs):
3729 pass
3730
3731
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003732def DownloadGerritHook(force):
3733 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003734
3735 Args:
3736 force: True to update hooks. False to install hooks if not present.
3737 """
3738 if not settings.GetIsGerrit():
3739 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003740 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003741 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3742 if not os.access(dst, os.X_OK):
3743 if os.path.exists(dst):
3744 if not force:
3745 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003746 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003747 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003748 if not hasSheBang(dst):
3749 DieWithError('Not a script: %s\n'
3750 'You need to download from\n%s\n'
3751 'into .git/hooks/commit-msg and '
3752 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003753 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3754 except Exception:
3755 if os.path.exists(dst):
3756 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003757 DieWithError('\nFailed to download hooks.\n'
3758 'You need to download from\n%s\n'
3759 'into .git/hooks/commit-msg and '
3760 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003761
3762
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003763def GetRietveldCodereviewSettingsInteractively():
3764 """Prompt the user for settings."""
3765 server = settings.GetDefaultServerUrl(error_ok=True)
3766 prompt = 'Rietveld server (host[:port])'
3767 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3768 newserver = ask_for_data(prompt + ':')
3769 if not server and not newserver:
3770 newserver = DEFAULT_SERVER
3771 if newserver:
3772 newserver = gclient_utils.UpgradeToHttps(newserver)
3773 if newserver != server:
3774 RunGit(['config', 'rietveld.server', newserver])
3775
3776 def SetProperty(initial, caption, name, is_url):
3777 prompt = caption
3778 if initial:
3779 prompt += ' ("x" to clear) [%s]' % initial
3780 new_val = ask_for_data(prompt + ':')
3781 if new_val == 'x':
3782 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3783 elif new_val:
3784 if is_url:
3785 new_val = gclient_utils.UpgradeToHttps(new_val)
3786 if new_val != initial:
3787 RunGit(['config', 'rietveld.' + name, new_val])
3788
3789 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3790 SetProperty(settings.GetDefaultPrivateFlag(),
3791 'Private flag (rietveld only)', 'private', False)
3792 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3793 'tree-status-url', False)
3794 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3795 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3796 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3797 'run-post-upload-hook', False)
3798
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003799
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003800class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003801 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003802
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003803 _GOOGLESOURCE = 'googlesource.com'
3804
3805 def __init__(self):
3806 # Cached list of [host, identity, source], where source is either
3807 # .gitcookies or .netrc.
3808 self._all_hosts = None
3809
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003810 def ensure_configured_gitcookies(self):
3811 """Runs checks and suggests fixes to make git use .gitcookies from default
3812 path."""
3813 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3814 configured_path = RunGitSilent(
3815 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003816 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003817 if configured_path:
3818 self._ensure_default_gitcookies_path(configured_path, default)
3819 else:
3820 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003821
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003822 @staticmethod
3823 def _ensure_default_gitcookies_path(configured_path, default_path):
3824 assert configured_path
3825 if configured_path == default_path:
3826 print('git is already configured to use your .gitcookies from %s' %
3827 configured_path)
3828 return
3829
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003830 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003831 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3832 (configured_path, default_path))
3833
3834 if not os.path.exists(configured_path):
3835 print('However, your configured .gitcookies file is missing.')
3836 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3837 action='reconfigure')
3838 RunGit(['config', '--global', 'http.cookiefile', default_path])
3839 return
3840
3841 if os.path.exists(default_path):
3842 print('WARNING: default .gitcookies file already exists %s' %
3843 default_path)
3844 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3845 default_path)
3846
3847 confirm_or_exit('Move existing .gitcookies to default location?',
3848 action='move')
3849 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003850 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003851 print('Moved and reconfigured git to use .gitcookies from %s' %
3852 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003853
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003854 @staticmethod
3855 def _configure_gitcookies_path(default_path):
3856 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3857 if os.path.exists(netrc_path):
3858 print('You seem to be using outdated .netrc for git credentials: %s' %
3859 netrc_path)
3860 print('This tool will guide you through setting up recommended '
3861 '.gitcookies store for git credentials.\n'
3862 '\n'
3863 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3864 ' git config --global --unset http.cookiefile\n'
3865 ' mv %s %s.backup\n\n' % (default_path, default_path))
3866 confirm_or_exit(action='setup .gitcookies')
3867 RunGit(['config', '--global', 'http.cookiefile', default_path])
3868 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003869
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003870 def get_hosts_with_creds(self, include_netrc=False):
3871 if self._all_hosts is None:
3872 a = gerrit_util.CookiesAuthenticator()
3873 self._all_hosts = [
3874 (h, u, s)
3875 for h, u, s in itertools.chain(
3876 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3877 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3878 )
3879 if h.endswith(self._GOOGLESOURCE)
3880 ]
3881
3882 if include_netrc:
3883 return self._all_hosts
3884 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3885
3886 def print_current_creds(self, include_netrc=False):
3887 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3888 if not hosts:
3889 print('No Git/Gerrit credentials found')
3890 return
3891 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3892 header = [('Host', 'User', 'Which file'),
3893 ['=' * l for l in lengths]]
3894 for row in (header + hosts):
3895 print('\t'.join((('%%+%ds' % l) % s)
3896 for l, s in zip(lengths, row)))
3897
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003898 @staticmethod
3899 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003900 """Parses identity "git-<username>.domain" into <username> and domain."""
3901 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003902 # distinguishable from sub-domains. But we do know typical domains:
3903 if identity.endswith('.chromium.org'):
3904 domain = 'chromium.org'
3905 username = identity[:-len('.chromium.org')]
3906 else:
3907 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003908 if username.startswith('git-'):
3909 username = username[len('git-'):]
3910 return username, domain
3911
3912 def _get_usernames_of_domain(self, domain):
3913 """Returns list of usernames referenced by .gitcookies in a given domain."""
3914 identities_by_domain = {}
3915 for _, identity, _ in self.get_hosts_with_creds():
3916 username, domain = self._parse_identity(identity)
3917 identities_by_domain.setdefault(domain, []).append(username)
3918 return identities_by_domain.get(domain)
3919
3920 def _canonical_git_googlesource_host(self, host):
3921 """Normalizes Gerrit hosts (with '-review') to Git host."""
3922 assert host.endswith(self._GOOGLESOURCE)
3923 # Prefix doesn't include '.' at the end.
3924 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3925 if prefix.endswith('-review'):
3926 prefix = prefix[:-len('-review')]
3927 return prefix + '.' + self._GOOGLESOURCE
3928
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003929 def _canonical_gerrit_googlesource_host(self, host):
3930 git_host = self._canonical_git_googlesource_host(host)
3931 prefix = git_host.split('.', 1)[0]
3932 return prefix + '-review.' + self._GOOGLESOURCE
3933
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003934 def _get_counterpart_host(self, host):
3935 assert host.endswith(self._GOOGLESOURCE)
3936 git = self._canonical_git_googlesource_host(host)
3937 gerrit = self._canonical_gerrit_googlesource_host(git)
3938 return git if gerrit == host else gerrit
3939
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003940 def has_generic_host(self):
3941 """Returns whether generic .googlesource.com has been configured.
3942
3943 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3944 """
3945 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3946 if host == '.' + self._GOOGLESOURCE:
3947 return True
3948 return False
3949
3950 def _get_git_gerrit_identity_pairs(self):
3951 """Returns map from canonic host to pair of identities (Git, Gerrit).
3952
3953 One of identities might be None, meaning not configured.
3954 """
3955 host_to_identity_pairs = {}
3956 for host, identity, _ in self.get_hosts_with_creds():
3957 canonical = self._canonical_git_googlesource_host(host)
3958 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3959 idx = 0 if canonical == host else 1
3960 pair[idx] = identity
3961 return host_to_identity_pairs
3962
3963 def get_partially_configured_hosts(self):
3964 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003965 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3966 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3967 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003968
3969 def get_conflicting_hosts(self):
3970 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003971 host
3972 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003973 if None not in (i1, i2) and i1 != i2)
3974
3975 def get_duplicated_hosts(self):
3976 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3977 return set(host for host, count in counters.iteritems() if count > 1)
3978
3979 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3980 'chromium.googlesource.com': 'chromium.org',
3981 'chrome-internal.googlesource.com': 'google.com',
3982 }
3983
3984 def get_hosts_with_wrong_identities(self):
3985 """Finds hosts which **likely** reference wrong identities.
3986
3987 Note: skips hosts which have conflicting identities for Git and Gerrit.
3988 """
3989 hosts = set()
3990 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3991 pair = self._get_git_gerrit_identity_pairs().get(host)
3992 if pair and pair[0] == pair[1]:
3993 _, domain = self._parse_identity(pair[0])
3994 if domain != expected:
3995 hosts.add(host)
3996 return hosts
3997
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003998 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003999 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004000 hosts = sorted(hosts)
4001 assert hosts
4002 if extra_column_func is None:
4003 extras = [''] * len(hosts)
4004 else:
4005 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004006 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
4007 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004008 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004009 lines.append(tmpl % he)
4010 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004011
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004012 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004013 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004014 yield ('.googlesource.com wildcard record detected',
4015 ['Chrome Infrastructure team recommends to list full host names '
4016 'explicitly.'],
4017 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004018
4019 dups = self.get_duplicated_hosts()
4020 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004021 yield ('The following hosts were defined twice',
4022 self._format_hosts(dups),
4023 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004024
4025 partial = self.get_partially_configured_hosts()
4026 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004027 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
4028 'These hosts are missing',
4029 self._format_hosts(partial, lambda host: 'but %s defined' %
4030 self._get_counterpart_host(host)),
4031 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004032
4033 conflicting = self.get_conflicting_hosts()
4034 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004035 yield ('The following Git hosts have differing credentials from their '
4036 'Gerrit counterparts',
4037 self._format_hosts(conflicting, lambda host: '%s vs %s' %
4038 tuple(self._get_git_gerrit_identity_pairs()[host])),
4039 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004040
4041 wrong = self.get_hosts_with_wrong_identities()
4042 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004043 yield ('These hosts likely use wrong identity',
4044 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
4045 (self._get_git_gerrit_identity_pairs()[host][0],
4046 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
4047 wrong)
4048
4049 def find_and_report_problems(self):
4050 """Returns True if there was at least one problem, else False."""
4051 found = False
4052 bad_hosts = set()
4053 for title, sublines, hosts in self._find_problems():
4054 if not found:
4055 found = True
4056 print('\n\n.gitcookies problem report:\n')
4057 bad_hosts.update(hosts or [])
4058 print(' %s%s' % (title , (':' if sublines else '')))
4059 if sublines:
4060 print()
4061 print(' %s' % '\n '.join(sublines))
4062 print()
4063
4064 if bad_hosts:
4065 assert found
4066 print(' You can manually remove corresponding lines in your %s file and '
4067 'visit the following URLs with correct account to generate '
4068 'correct credential lines:\n' %
4069 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
4070 print(' %s' % '\n '.join(sorted(set(
4071 gerrit_util.CookiesAuthenticator().get_new_password_url(
4072 self._canonical_git_googlesource_host(host))
4073 for host in bad_hosts
4074 ))))
4075 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004076
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004077
4078def CMDcreds_check(parser, args):
4079 """Checks credentials and suggests changes."""
4080 _, _ = parser.parse_args(args)
4081
4082 if gerrit_util.GceAuthenticator.is_gce():
Aaron Gabled10ca0e2017-09-11 11:24:10 -07004083 DieWithError(
4084 'This command is not designed for GCE, are you on a bot?\n'
4085 'If you need to run this, export SKIP_GCE_AUTH_FOR_GIT=1 in your env.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004086
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01004087 checker = _GitCookiesChecker()
4088 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004089
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004090 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004091 checker.print_current_creds(include_netrc=True)
4092
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004093 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004094 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004095 return 0
4096 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004097
4098
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004099@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004100def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004101 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004102
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004103 print('WARNING: git cl config works for Rietveld only.')
tandrii5d0a0422016-09-14 06:24:35 -07004104 # TODO(tandrii): remove this once we switch to Gerrit.
4105 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00004106 parser.add_option('--activate-update', action='store_true',
4107 help='activate auto-updating [rietveld] section in '
4108 '.git/config')
4109 parser.add_option('--deactivate-update', action='store_true',
4110 help='deactivate auto-updating [rietveld] section in '
4111 '.git/config')
4112 options, args = parser.parse_args(args)
4113
4114 if options.deactivate_update:
4115 RunGit(['config', 'rietveld.autoupdate', 'false'])
4116 return
4117
4118 if options.activate_update:
4119 RunGit(['config', '--unset', 'rietveld.autoupdate'])
4120 return
4121
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004122 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00004123 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004124 return 0
4125
4126 url = args[0]
4127 if not url.endswith('codereview.settings'):
4128 url = os.path.join(url, 'codereview.settings')
4129
4130 # Load code review settings and download hooks (if available).
4131 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
4132 return 0
4133
4134
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004135def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004136 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004137 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
4138 branch = ShortBranchName(branchref)
4139 _, args = parser.parse_args(args)
4140 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07004141 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004142 return RunGit(['config', 'branch.%s.base-url' % branch],
4143 error_ok=False).strip()
4144 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004145 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004146 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
4147 error_ok=False).strip()
4148
4149
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004150def color_for_status(status):
4151 """Maps a Changelist status to color, for CMDstatus and other tools."""
4152 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07004153 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004154 'waiting': Fore.BLUE,
4155 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07004156 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004157 'lgtm': Fore.GREEN,
4158 'commit': Fore.MAGENTA,
4159 'closed': Fore.CYAN,
4160 'error': Fore.WHITE,
4161 }.get(status, Fore.WHITE)
4162
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00004163
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004164def get_cl_statuses(changes, fine_grained, max_processes=None):
4165 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004166
4167 If fine_grained is true, this will fetch CL statuses from the server.
4168 Otherwise, simply indicate if there's a matching url for the given branches.
4169
4170 If max_processes is specified, it is used as the maximum number of processes
4171 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
4172 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004173
4174 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004175 """
qyearsley12fa6ff2016-08-24 09:18:40 -07004176 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004177 upload.verbosity = 0
4178
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004179 if not changes:
4180 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004181
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004182 if not fine_grained:
4183 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07004184 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004185 for cl in changes:
4186 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004187 return
4188
4189 # First, sort out authentication issues.
4190 logging.debug('ensuring credentials exist')
4191 for cl in changes:
4192 cl.EnsureAuthenticated(force=False, refresh=True)
4193
4194 def fetch(cl):
4195 try:
4196 return (cl, cl.GetStatus())
4197 except:
4198 # See http://crbug.com/629863.
4199 logging.exception('failed to fetch status for %s:', cl)
4200 raise
4201
4202 threads_count = len(changes)
4203 if max_processes:
4204 threads_count = max(1, min(threads_count, max_processes))
4205 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
4206
4207 pool = ThreadPool(threads_count)
4208 fetched_cls = set()
4209 try:
4210 it = pool.imap_unordered(fetch, changes).__iter__()
4211 while True:
4212 try:
4213 cl, status = it.next(timeout=5)
4214 except multiprocessing.TimeoutError:
4215 break
4216 fetched_cls.add(cl)
4217 yield cl, status
4218 finally:
4219 pool.close()
4220
4221 # Add any branches that failed to fetch.
4222 for cl in set(changes) - fetched_cls:
4223 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004224
rmistry@google.com2dd99862015-06-22 12:22:18 +00004225
4226def upload_branch_deps(cl, args):
4227 """Uploads CLs of local branches that are dependents of the current branch.
4228
4229 If the local branch dependency tree looks like:
4230 test1 -> test2.1 -> test3.1
4231 -> test3.2
4232 -> test2.2 -> test3.3
4233
4234 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
4235 run on the dependent branches in this order:
4236 test2.1, test3.1, test3.2, test2.2, test3.3
4237
4238 Note: This function does not rebase your local dependent branches. Use it when
4239 you make a change to the parent branch that will not conflict with its
4240 dependent branches, and you would like their dependencies updated in
4241 Rietveld.
4242 """
4243 if git_common.is_dirty_git_tree('upload-branch-deps'):
4244 return 1
4245
4246 root_branch = cl.GetBranch()
4247 if root_branch is None:
4248 DieWithError('Can\'t find dependent branches from detached HEAD state. '
4249 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01004250 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00004251 DieWithError('Current branch does not have an uploaded CL. We cannot set '
4252 'patchset dependencies without an uploaded CL.')
4253
4254 branches = RunGit(['for-each-ref',
4255 '--format=%(refname:short) %(upstream:short)',
4256 'refs/heads'])
4257 if not branches:
4258 print('No local branches found.')
4259 return 0
4260
4261 # Create a dictionary of all local branches to the branches that are dependent
4262 # on it.
4263 tracked_to_dependents = collections.defaultdict(list)
4264 for b in branches.splitlines():
4265 tokens = b.split()
4266 if len(tokens) == 2:
4267 branch_name, tracked = tokens
4268 tracked_to_dependents[tracked].append(branch_name)
4269
vapiera7fbd5a2016-06-16 09:17:49 -07004270 print()
4271 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004272 dependents = []
4273 def traverse_dependents_preorder(branch, padding=''):
4274 dependents_to_process = tracked_to_dependents.get(branch, [])
4275 padding += ' '
4276 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07004277 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00004278 dependents.append(dependent)
4279 traverse_dependents_preorder(dependent, padding)
4280 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07004281 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004282
4283 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004284 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004285 return 0
4286
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01004287 confirm_or_exit('This command will checkout all dependent branches and run '
4288 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004289
andybons@chromium.org962f9462016-02-03 20:00:42 +00004290 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00004291 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00004292 args.extend(['-t', 'Updated patchset dependency'])
4293
rmistry@google.com2dd99862015-06-22 12:22:18 +00004294 # Record all dependents that failed to upload.
4295 failures = {}
4296 # Go through all dependents, checkout the branch and upload.
4297 try:
4298 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004299 print()
4300 print('--------------------------------------')
4301 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004302 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07004303 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004304 try:
4305 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004306 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004307 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004308 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00004309 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07004310 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004311 finally:
4312 # Swap back to the original root branch.
4313 RunGit(['checkout', '-q', root_branch])
4314
vapiera7fbd5a2016-06-16 09:17:49 -07004315 print()
4316 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004317 for dependent_branch in dependents:
4318 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07004319 print(' %s : %s' % (dependent_branch, upload_status))
4320 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004321
4322 return 0
4323
4324
kmarshall3bff56b2016-06-06 18:31:47 -07004325def CMDarchive(parser, args):
4326 """Archives and deletes branches associated with closed changelists."""
4327 parser.add_option(
4328 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07004329 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07004330 parser.add_option(
4331 '-f', '--force', action='store_true',
4332 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07004333 parser.add_option(
4334 '-d', '--dry-run', action='store_true',
4335 help='Skip the branch tagging and removal steps.')
4336 parser.add_option(
4337 '-t', '--notags', action='store_true',
4338 help='Do not tag archived branches. '
4339 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004340
4341 auth.add_auth_options(parser)
4342 options, args = parser.parse_args(args)
4343 if args:
4344 parser.error('Unsupported args: %s' % ' '.join(args))
4345 auth_config = auth.extract_auth_config_from_options(options)
4346
4347 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4348 if not branches:
4349 return 0
4350
vapiera7fbd5a2016-06-16 09:17:49 -07004351 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004352 changes = [Changelist(branchref=b, auth_config=auth_config)
4353 for b in branches.splitlines()]
4354 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4355 statuses = get_cl_statuses(changes,
4356 fine_grained=True,
4357 max_processes=options.maxjobs)
4358 proposal = [(cl.GetBranch(),
4359 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4360 for cl, status in statuses
4361 if status == 'closed']
4362 proposal.sort()
4363
4364 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004365 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004366 return 0
4367
4368 current_branch = GetCurrentBranch()
4369
vapiera7fbd5a2016-06-16 09:17:49 -07004370 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004371 if options.notags:
4372 for next_item in proposal:
4373 print(' ' + next_item[0])
4374 else:
4375 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4376 for next_item in proposal:
4377 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004378
kmarshall9249e012016-08-23 12:02:16 -07004379 # Quit now on precondition failure or if instructed by the user, either
4380 # via an interactive prompt or by command line flags.
4381 if options.dry_run:
4382 print('\nNo changes were made (dry run).\n')
4383 return 0
4384 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004385 print('You are currently on a branch \'%s\' which is associated with a '
4386 'closed codereview issue, so archive cannot proceed. Please '
4387 'checkout another branch and run this command again.' %
4388 current_branch)
4389 return 1
kmarshall9249e012016-08-23 12:02:16 -07004390 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004391 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4392 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004393 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004394 return 1
4395
4396 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004397 if not options.notags:
4398 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004399 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004400
vapiera7fbd5a2016-06-16 09:17:49 -07004401 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004402
4403 return 0
4404
4405
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004406def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004407 """Show status of changelists.
4408
4409 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004410 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004411 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004412 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004413 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004414 - Magenta in the commit queue
4415 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004416 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004417
4418 Also see 'git cl comments'.
4419 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004420 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004421 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004422 parser.add_option('-f', '--fast', action='store_true',
4423 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004424 parser.add_option(
4425 '-j', '--maxjobs', action='store', type=int,
4426 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004427
4428 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004429 _add_codereview_issue_select_options(
4430 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004431 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004432 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004433 if args:
4434 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004435 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004436
iannuccie53c9352016-08-17 14:40:40 -07004437 if options.issue is not None and not options.field:
4438 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004439
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004440 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004441 cl = Changelist(auth_config=auth_config, issue=options.issue,
4442 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004443 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004444 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004445 elif options.field == 'id':
4446 issueid = cl.GetIssue()
4447 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004448 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004449 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004450 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004451 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004452 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004453 elif options.field == 'status':
4454 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004455 elif options.field == 'url':
4456 url = cl.GetIssueURL()
4457 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004458 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004459 return 0
4460
4461 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4462 if not branches:
4463 print('No local branch found.')
4464 return 0
4465
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004466 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004467 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004468 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004469 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004470 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004471 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004472 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004473
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004474 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004475 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
4476 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4477 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004478 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004479 c, status = output.next()
4480 branch_statuses[c.GetBranch()] = status
4481 status = branch_statuses.pop(branch)
4482 url = cl.GetIssueURL()
4483 if url and (not status or status == 'error'):
4484 # The issue probably doesn't exist anymore.
4485 url += ' (broken)'
4486
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004487 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004488 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004489 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004490 color = ''
4491 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004492 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07004493 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004494 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07004495 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004496
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004497
4498 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07004499 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004500 print('Current branch: %s' % branch)
4501 for cl in changes:
4502 if cl.GetBranch() == branch:
4503 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004504 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004505 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004506 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004507 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004508 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004509 print('Issue description:')
4510 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004511 return 0
4512
4513
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004514def colorize_CMDstatus_doc():
4515 """To be called once in main() to add colors to git cl status help."""
4516 colors = [i for i in dir(Fore) if i[0].isupper()]
4517
4518 def colorize_line(line):
4519 for color in colors:
4520 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004521 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004522 indent = len(line) - len(line.lstrip(' ')) + 1
4523 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4524 return line
4525
4526 lines = CMDstatus.__doc__.splitlines()
4527 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4528
4529
phajdan.jre328cf92016-08-22 04:12:17 -07004530def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004531 if path == '-':
4532 json.dump(contents, sys.stdout)
4533 else:
4534 with open(path, 'w') as f:
4535 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004536
4537
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004538@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004539def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004540 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004541
4542 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004543 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004544 parser.add_option('-r', '--reverse', action='store_true',
4545 help='Lookup the branch(es) for the specified issues. If '
4546 'no issues are specified, all branches with mapped '
4547 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004548 parser.add_option('--json',
4549 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004550 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004551 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004552 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004553
dnj@chromium.org406c4402015-03-03 17:22:28 +00004554 if options.reverse:
4555 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004556 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004557 # Reverse issue lookup.
4558 issue_branch_map = {}
4559 for branch in branches:
4560 cl = Changelist(branchref=branch)
4561 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
4562 if not args:
4563 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004564 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004565 for issue in args:
4566 if not issue:
4567 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004568 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004569 print('Branch for issue number %s: %s' % (
4570 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004571 if options.json:
4572 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004573 return 0
4574
4575 if len(args) > 0:
4576 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4577 if not issue.valid:
4578 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4579 'or no argument to list it.\n'
4580 'Maybe you want to run git cl status?')
4581 cl = Changelist(codereview=issue.codereview)
4582 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004583 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004584 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004585 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4586 if options.json:
4587 write_json(options.json, {
4588 'issue': cl.GetIssue(),
4589 'issue_url': cl.GetIssueURL(),
4590 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004591 return 0
4592
4593
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004594def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004595 """Shows or posts review comments for any changelist."""
4596 parser.add_option('-a', '--add-comment', dest='comment',
4597 help='comment to add to an issue')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004598 parser.add_option('-i', '--issue', dest='issue',
4599 help='review issue id (defaults to current issue). '
4600 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004601 parser.add_option('-m', '--machine-readable', dest='readable',
4602 action='store_false', default=True,
4603 help='output comments in a format compatible with '
4604 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004605 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004606 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004607 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004608 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004609 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004610 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004611 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004612
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004613 issue = None
4614 if options.issue:
4615 try:
4616 issue = int(options.issue)
4617 except ValueError:
4618 DieWithError('A review issue id is expected to be a number')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004619 if not options.forced_codereview:
4620 parser.error('--gerrit or --rietveld is required if --issue is specified')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004621
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004622 cl = Changelist(issue=issue,
Andrii Shyshkalov70909e12017-04-10 14:38:32 +02004623 codereview=options.forced_codereview,
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004624 auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004625
4626 if options.comment:
4627 cl.AddComment(options.comment)
4628 return 0
4629
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004630 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4631 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004632 for comment in summary:
4633 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004634 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004635 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004636 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004637 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004638 color = Fore.MAGENTA
4639 else:
4640 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004641 print('\n%s%s %s%s\n%s' % (
4642 color,
4643 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4644 comment.sender,
4645 Fore.RESET,
4646 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4647
smut@google.comc85ac942015-09-15 16:34:43 +00004648 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004649 def pre_serialize(c):
4650 dct = c.__dict__.copy()
4651 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4652 return dct
smut@google.comc85ac942015-09-15 16:34:43 +00004653 with open(options.json_file, 'wb') as f:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004654 json.dump(map(pre_serialize, summary), f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004655 return 0
4656
4657
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004658@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004659def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004660 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004661 parser.add_option('-d', '--display', action='store_true',
4662 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004663 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004664 help='New description to set for this issue (- for stdin, '
4665 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004666 parser.add_option('-f', '--force', action='store_true',
4667 help='Delete any unpublished Gerrit edits for this issue '
4668 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004669
4670 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004671 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004672 options, args = parser.parse_args(args)
4673 _process_codereview_select_options(parser, options)
4674
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004675 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004676 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004677 target_issue_arg = ParseIssueNumberArgument(args[0],
4678 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004679 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004680 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004681
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004682 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004683
martiniss6eda05f2016-06-30 10:18:35 -07004684 kwargs = {
4685 'auth_config': auth_config,
4686 'codereview': options.forced_codereview,
4687 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004688 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004689 if target_issue_arg:
4690 kwargs['issue'] = target_issue_arg.issue
4691 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004692 if target_issue_arg.codereview and not options.forced_codereview:
4693 detected_codereview_from_url = True
4694 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004695
4696 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004697 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004698 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004699 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004700
4701 if detected_codereview_from_url:
4702 logging.info('canonical issue/change URL: %s (type: %s)\n',
4703 cl.GetIssueURL(), target_issue_arg.codereview)
4704
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004705 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004706
smut@google.com34fb6b12015-07-13 20:03:26 +00004707 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004708 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004709 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004710
4711 if options.new_description:
4712 text = options.new_description
4713 if text == '-':
4714 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004715 elif text == '+':
4716 base_branch = cl.GetCommonAncestorWithUpstream()
4717 change = cl.GetChange(base_branch, None, local_description=True)
4718 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004719
4720 description.set_description(text)
4721 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004722 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004723
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004724 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004725 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004726 return 0
4727
4728
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004729def CreateDescriptionFromLog(args):
4730 """Pulls out the commit log to use as a base for the CL description."""
4731 log_args = []
4732 if len(args) == 1 and not args[0].endswith('.'):
4733 log_args = [args[0] + '..']
4734 elif len(args) == 1 and args[0].endswith('...'):
4735 log_args = [args[0][:-1]]
4736 elif len(args) == 2:
4737 log_args = [args[0] + '..' + args[1]]
4738 else:
4739 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004740 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004741
4742
thestig@chromium.org44202a22014-03-11 19:22:18 +00004743def CMDlint(parser, args):
4744 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004745 parser.add_option('--filter', action='append', metavar='-x,+y',
4746 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004747 auth.add_auth_options(parser)
4748 options, args = parser.parse_args(args)
4749 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004750
4751 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004752 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004753 try:
4754 import cpplint
4755 import cpplint_chromium
4756 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004757 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004758 return 1
4759
4760 # Change the current working directory before calling lint so that it
4761 # shows the correct base.
4762 previous_cwd = os.getcwd()
4763 os.chdir(settings.GetRoot())
4764 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004765 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004766 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4767 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004768 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004769 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004770 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004771
4772 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004773 command = args + files
4774 if options.filter:
4775 command = ['--filter=' + ','.join(options.filter)] + command
4776 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004777
4778 white_regex = re.compile(settings.GetLintRegex())
4779 black_regex = re.compile(settings.GetLintIgnoreRegex())
4780 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4781 for filename in filenames:
4782 if white_regex.match(filename):
4783 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004784 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004785 else:
4786 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4787 extra_check_functions)
4788 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004789 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004790 finally:
4791 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004792 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004793 if cpplint._cpplint_state.error_count != 0:
4794 return 1
4795 return 0
4796
4797
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004798def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004799 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004800 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004801 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004802 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004803 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004804 parser.add_option('--all', action='store_true',
4805 help='Run checks against all files, not just modified ones')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004806 auth.add_auth_options(parser)
4807 options, args = parser.parse_args(args)
4808 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004809
sbc@chromium.org71437c02015-04-09 19:29:40 +00004810 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004811 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004812 return 1
4813
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004814 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004815 if args:
4816 base_branch = args[0]
4817 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004818 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004819 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004820
Aaron Gable8076c282017-11-29 14:39:41 -08004821 if options.all:
4822 base_change = cl.GetChange(base_branch, None)
4823 files = [('M', f) for f in base_change.AllFiles()]
4824 change = presubmit_support.GitChange(
4825 base_change.Name(),
4826 base_change.FullDescriptionText(),
4827 base_change.RepositoryRoot(),
4828 files,
4829 base_change.issue,
4830 base_change.patchset,
4831 base_change.author_email,
4832 base_change._upstream)
4833 else:
4834 change = cl.GetChange(base_branch, None)
4835
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004836 cl.RunHook(
4837 committing=not options.upload,
4838 may_prompt=False,
4839 verbose=options.verbose,
Aaron Gable8076c282017-11-29 14:39:41 -08004840 change=change)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004841 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004842
4843
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004844def GenerateGerritChangeId(message):
4845 """Returns Ixxxxxx...xxx change id.
4846
4847 Works the same way as
4848 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4849 but can be called on demand on all platforms.
4850
4851 The basic idea is to generate git hash of a state of the tree, original commit
4852 message, author/committer info and timestamps.
4853 """
4854 lines = []
4855 tree_hash = RunGitSilent(['write-tree'])
4856 lines.append('tree %s' % tree_hash.strip())
4857 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4858 if code == 0:
4859 lines.append('parent %s' % parent.strip())
4860 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4861 lines.append('author %s' % author.strip())
4862 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4863 lines.append('committer %s' % committer.strip())
4864 lines.append('')
4865 # Note: Gerrit's commit-hook actually cleans message of some lines and
4866 # whitespace. This code is not doing this, but it clearly won't decrease
4867 # entropy.
4868 lines.append(message)
4869 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4870 stdin='\n'.join(lines))
4871 return 'I%s' % change_hash.strip()
4872
4873
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004874def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004875 """Computes the remote branch ref to use for the CL.
4876
4877 Args:
4878 remote (str): The git remote for the CL.
4879 remote_branch (str): The git remote branch for the CL.
4880 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004881 """
4882 if not (remote and remote_branch):
4883 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004884
wittman@chromium.org455dc922015-01-26 20:15:50 +00004885 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004886 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004887 # refs, which are then translated into the remote full symbolic refs
4888 # below.
4889 if '/' not in target_branch:
4890 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4891 else:
4892 prefix_replacements = (
4893 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4894 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4895 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4896 )
4897 match = None
4898 for regex, replacement in prefix_replacements:
4899 match = re.search(regex, target_branch)
4900 if match:
4901 remote_branch = target_branch.replace(match.group(0), replacement)
4902 break
4903 if not match:
4904 # This is a branch path but not one we recognize; use as-is.
4905 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004906 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4907 # Handle the refs that need to land in different refs.
4908 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004909
wittman@chromium.org455dc922015-01-26 20:15:50 +00004910 # Create the true path to the remote branch.
4911 # Does the following translation:
4912 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4913 # * refs/remotes/origin/master -> refs/heads/master
4914 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4915 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4916 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4917 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4918 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4919 'refs/heads/')
4920 elif remote_branch.startswith('refs/remotes/branch-heads'):
4921 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004922
wittman@chromium.org455dc922015-01-26 20:15:50 +00004923 return remote_branch
4924
4925
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004926def cleanup_list(l):
4927 """Fixes a list so that comma separated items are put as individual items.
4928
4929 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4930 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4931 """
4932 items = sum((i.split(',') for i in l), [])
4933 stripped_items = (i.strip() for i in items)
4934 return sorted(filter(None, stripped_items))
4935
4936
Aaron Gable4db38df2017-11-03 14:59:07 -07004937@subcommand.usage('[flags]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004938def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004939 """Uploads the current changelist to codereview.
4940
4941 Can skip dependency patchset uploads for a branch by running:
4942 git config branch.branch_name.skip-deps-uploads True
4943 To unset run:
4944 git config --unset branch.branch_name.skip-deps-uploads
4945 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004946
4947 If the name of the checked out branch starts with "bug-" or "fix-" followed by
4948 a bug number, this bug number is automatically populated in the CL
4949 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004950
4951 If subject contains text in square brackets or has "<text>: " prefix, such
4952 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
4953 [git-cl] add support for hashtags
4954 Foo bar: implement foo
4955 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004956 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004957 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4958 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004959 parser.add_option('--bypass-watchlists', action='store_true',
4960 dest='bypass_watchlists',
4961 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004962 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004963 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004964 parser.add_option('--message', '-m', dest='message',
4965 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004966 parser.add_option('-b', '--bug',
4967 help='pre-populate the bug number(s) for this issue. '
4968 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004969 parser.add_option('--message-file', dest='message_file',
4970 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004971 parser.add_option('--title', '-t', dest='title',
4972 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004973 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004974 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004975 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004976 parser.add_option('--tbrs',
4977 action='append', default=[],
4978 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004979 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004980 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004981 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004982 parser.add_option('--hashtag', dest='hashtags',
4983 action='append', default=[],
4984 help=('Gerrit hashtag for new CL; '
4985 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004986 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004987 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004988 parser.add_option('--emulate_svn_auto_props',
4989 '--emulate-svn-auto-props',
4990 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004991 dest="emulate_svn_auto_props",
4992 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004993 parser.add_option('-c', '--use-commit-queue', action='store_true',
Aaron Gableedbc4132017-09-11 13:22:28 -07004994 help='tell the commit queue to commit this patchset; '
4995 'implies --send-mail')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004996 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004997 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004998 metavar='TARGET',
4999 help='Apply CL to remote ref TARGET. ' +
5000 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00005001 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005002 help='Squash multiple commits into one')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00005003 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005004 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07005005 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005006 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07005007 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
5008 const='TBR', help='add a set of OWNERS to TBR')
5009 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
5010 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00005011 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
5012 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00005013 help='Send the patchset to do a CQ dry run right after '
5014 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00005015 parser.add_option('--dependencies', action='store_true',
5016 help='Uploads CLs of all the local branches that depend on '
5017 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00005018
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005019 # TODO: remove Rietveld flags
5020 parser.add_option('--private', action='store_true',
5021 help='set the review private (rietveld only)')
5022 parser.add_option('--email', default=None,
5023 help='email address to use to connect to Rietveld')
5024
rmistry@google.com2dd99862015-06-22 12:22:18 +00005025 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00005026 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005027 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005028 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005029 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005030 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005031 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005032
sbc@chromium.org71437c02015-04-09 19:29:40 +00005033 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00005034 return 1
5035
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005036 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07005037 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005038 options.cc = cleanup_list(options.cc)
5039
tandriib80458a2016-06-23 12:20:07 -07005040 if options.message_file:
5041 if options.message:
5042 parser.error('only one of --message and --message-file allowed.')
5043 options.message = gclient_utils.FileRead(options.message_file)
5044 options.message_file = None
5045
tandrii4d0545a2016-07-06 03:56:49 -07005046 if options.cq_dry_run and options.use_commit_queue:
5047 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
5048
Aaron Gableedbc4132017-09-11 13:22:28 -07005049 if options.use_commit_queue:
5050 options.send_mail = True
5051
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00005052 # For sanity of test expectations, do this otherwise lazy-loading *now*.
5053 settings.GetIsGerrit()
5054
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005055 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00005056 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005057
5058
Francois Dorayd42c6812017-05-30 15:10:20 -04005059@subcommand.usage('--description=<description file>')
5060def CMDsplit(parser, args):
5061 """Splits a branch into smaller branches and uploads CLs.
5062
5063 Creates a branch and uploads a CL for each group of files modified in the
5064 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07005065 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04005066 the shared OWNERS file.
5067 """
5068 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05005069 help="A text file containing a CL description in which "
5070 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005071 parser.add_option("-c", "--comment", dest="comment_file",
5072 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11005073 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
5074 default=False,
5075 help="List the files and reviewers for each CL that would "
5076 "be created, but don't create branches or CLs.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005077 options, _ = parser.parse_args(args)
5078
5079 if not options.description_file:
5080 parser.error('No --description flag specified.')
5081
5082 def WrappedCMDupload(args):
5083 return CMDupload(OptionParser(), args)
5084
5085 return split_cl.SplitCl(options.description_file, options.comment_file,
Chris Watkinsba28e462017-12-13 11:22:17 +11005086 Changelist, WrappedCMDupload, options.dry_run)
Francois Dorayd42c6812017-05-30 15:10:20 -04005087
5088
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005089@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005090def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005091 """DEPRECATED: Used to commit the current changelist via git-svn."""
5092 message = ('git-cl no longer supports committing to SVN repositories via '
5093 'git-svn. You probably want to use `git cl land` instead.')
5094 print(message)
5095 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005096
5097
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005098# Two special branches used by git cl land.
5099MERGE_BRANCH = 'git-cl-commit'
5100CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
5101
5102
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005103@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00005104def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005105 """Commits the current changelist via git.
5106
5107 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
5108 upstream and closes the issue automatically and atomically.
5109
5110 Otherwise (in case of Rietveld):
5111 Squashes branch into a single commit.
5112 Updates commit message with metadata (e.g. pointer to review).
5113 Pushes the code upstream.
5114 Updates review and closes.
5115 """
5116 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
5117 help='bypass upload presubmit hook')
5118 parser.add_option('-m', dest='message',
5119 help="override review description")
Aaron Gablef7543cd2017-07-20 14:26:31 -07005120 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005121 help="force yes to questions (don't prompt)")
5122 parser.add_option('-c', dest='contributor',
5123 help="external contributor for patch (appended to " +
5124 "description and used as author for git). Should be " +
5125 "formatted as 'First Last <email@example.com>'")
5126 add_git_similarity(parser)
5127 auth.add_auth_options(parser)
5128 (options, args) = parser.parse_args(args)
5129 auth_config = auth.extract_auth_config_from_options(options)
5130
5131 cl = Changelist(auth_config=auth_config)
5132
5133 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
5134 if cl.IsGerrit():
5135 if options.message:
5136 # This could be implemented, but it requires sending a new patch to
5137 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
5138 # Besides, Gerrit has the ability to change the commit message on submit
5139 # automatically, thus there is no need to support this option (so far?).
5140 parser.error('-m MESSAGE option is not supported for Gerrit.')
5141 if options.contributor:
5142 parser.error(
5143 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
5144 'Before uploading a commit to Gerrit, ensure it\'s author field is '
5145 'the contributor\'s "name <email>". If you can\'t upload such a '
5146 'commit for review, contact your repository admin and request'
5147 '"Forge-Author" permission.')
5148 if not cl.GetIssue():
5149 DieWithError('You must upload the change first to Gerrit.\n'
5150 ' If you would rather have `git cl land` upload '
5151 'automatically for you, see http://crbug.com/642759')
5152 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
5153 options.verbose)
5154
5155 current = cl.GetBranch()
5156 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
5157 if remote == '.':
5158 print()
5159 print('Attempting to push branch %r into another local branch!' % current)
5160 print()
5161 print('Either reparent this branch on top of origin/master:')
5162 print(' git reparent-branch --root')
5163 print()
5164 print('OR run `git rebase-update` if you think the parent branch is ')
5165 print('already committed.')
5166 print()
5167 print(' Current parent: %r' % upstream_branch)
5168 return 1
5169
5170 if not args:
5171 # Default to merging against our best guess of the upstream branch.
5172 args = [cl.GetUpstreamBranch()]
5173
5174 if options.contributor:
5175 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07005176 print("Please provide contributor as 'First Last <email@example.com>'")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005177 return 1
5178
5179 base_branch = args[0]
5180
5181 if git_common.is_dirty_git_tree('land'):
5182 return 1
5183
5184 # This rev-list syntax means "show all commits not in my branch that
5185 # are in base_branch".
5186 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
5187 base_branch]).splitlines()
5188 if upstream_commits:
5189 print('Base branch "%s" has %d commits '
5190 'not in this branch.' % (base_branch, len(upstream_commits)))
5191 print('Run "git merge %s" before attempting to land.' % base_branch)
5192 return 1
5193
5194 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
5195 if not options.bypass_hooks:
5196 author = None
5197 if options.contributor:
5198 author = re.search(r'\<(.*)\>', options.contributor).group(1)
5199 hook_results = cl.RunHook(
5200 committing=True,
5201 may_prompt=not options.force,
5202 verbose=options.verbose,
5203 change=cl.GetChange(merge_base, author))
5204 if not hook_results.should_continue():
5205 return 1
5206
5207 # Check the tree status if the tree status URL is set.
5208 status = GetTreeStatus()
5209 if 'closed' == status:
5210 print('The tree is closed. Please wait for it to reopen. Use '
5211 '"git cl land --bypass-hooks" to commit on a closed tree.')
5212 return 1
5213 elif 'unknown' == status:
5214 print('Unable to determine tree status. Please verify manually and '
5215 'use "git cl land --bypass-hooks" to commit on a closed tree.')
5216 return 1
5217
5218 change_desc = ChangeDescription(options.message)
5219 if not change_desc.description and cl.GetIssue():
5220 change_desc = ChangeDescription(cl.GetDescription())
5221
5222 if not change_desc.description:
5223 if not cl.GetIssue() and options.bypass_hooks:
5224 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
5225 else:
5226 print('No description set.')
5227 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
5228 return 1
5229
5230 # Keep a separate copy for the commit message, because the commit message
5231 # contains the link to the Rietveld issue, while the Rietveld message contains
5232 # the commit viewvc url.
5233 if cl.GetIssue():
Aaron Gablea1bab272017-04-11 16:38:18 -07005234 change_desc.update_reviewers(
Robert Iannucci6c98dc62017-04-18 11:38:00 -07005235 get_approving_reviewers(cl.GetIssueProperties()), [])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005236
5237 commit_desc = ChangeDescription(change_desc.description)
5238 if cl.GetIssue():
5239 # Xcode won't linkify this URL unless there is a non-whitespace character
5240 # after it. Add a period on a new line to circumvent this. Also add a space
5241 # before the period to make sure that Gitiles continues to correctly resolve
5242 # the URL.
5243 commit_desc.append_footer('Review-Url: %s .' % cl.GetIssueURL())
5244 if options.contributor:
5245 commit_desc.append_footer('Patch from %s.' % options.contributor)
5246
5247 print('Description:')
5248 print(commit_desc.description)
5249
5250 branches = [merge_base, cl.GetBranchRef()]
5251 if not options.force:
5252 print_stats(options.similarity, options.find_copies, branches)
5253
5254 # We want to squash all this branch's commits into one commit with the proper
5255 # description. We do this by doing a "reset --soft" to the base branch (which
5256 # keeps the working copy the same), then landing that.
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005257 # Delete the special branches if they exist.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005258 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
5259 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
5260 result = RunGitWithCode(showref_cmd)
5261 if result[0] == 0:
5262 RunGit(['branch', '-D', branch])
5263
5264 # We might be in a directory that's present in this branch but not in the
5265 # trunk. Move up to the top of the tree so that git commands that expect a
5266 # valid CWD won't fail after we check out the merge branch.
5267 rel_base_path = settings.GetRelativeRoot()
5268 if rel_base_path:
5269 os.chdir(rel_base_path)
5270
5271 # Stuff our change into the merge branch.
5272 # We wrap in a try...finally block so if anything goes wrong,
5273 # we clean up the branches.
5274 retcode = -1
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005275 revision = None
5276 try:
5277 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
5278 RunGit(['reset', '--soft', merge_base])
5279 if options.contributor:
5280 RunGit(
5281 [
5282 'commit', '--author', options.contributor,
5283 '-m', commit_desc.description,
5284 ])
5285 else:
5286 RunGit(['commit', '-m', commit_desc.description])
5287
5288 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
5289 mirror = settings.GetGitMirror(remote)
5290 if mirror:
5291 pushurl = mirror.url
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005292 git_numberer_enabled = _is_git_numberer_enabled(pushurl, branch)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005293 else:
5294 pushurl = remote # Usually, this is 'origin'.
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005295 git_numberer_enabled = _is_git_numberer_enabled(
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005296 RunGit(['config', 'remote.%s.url' % remote]).strip(), branch)
5297
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005298 retcode = PushToGitWithAutoRebase(
5299 pushurl, branch, commit_desc.description, git_numberer_enabled)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005300 if retcode == 0:
5301 revision = RunGit(['rev-parse', 'HEAD']).strip()
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005302 if git_numberer_enabled:
5303 change_desc = ChangeDescription(
5304 RunGit(['show', '-s', '--format=%B', 'HEAD']).strip())
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005305 except: # pylint: disable=bare-except
5306 if _IS_BEING_TESTED:
5307 logging.exception('this is likely your ACTUAL cause of test failure.\n'
5308 + '-' * 30 + '8<' + '-' * 30)
5309 logging.error('\n' + '-' * 30 + '8<' + '-' * 30 + '\n\n\n')
5310 raise
5311 finally:
5312 # And then swap back to the original branch and clean up.
5313 RunGit(['checkout', '-q', cl.GetBranch()])
5314 RunGit(['branch', '-D', MERGE_BRANCH])
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005315 RunGit(['branch', '-D', CHERRY_PICK_BRANCH], error_ok=True)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005316
5317 if not revision:
5318 print('Failed to push. If this persists, please file a bug.')
5319 return 1
5320
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005321 if cl.GetIssue():
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005322 viewvc_url = settings.GetViewVCUrl()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005323 if viewvc_url and revision:
5324 change_desc.append_footer(
5325 'Committed: %s%s' % (viewvc_url, revision))
5326 elif revision:
5327 change_desc.append_footer('Committed: %s' % (revision,))
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005328 print('Closing issue '
5329 '(you may be prompted for your codereview password)...')
5330 cl.UpdateDescription(change_desc.description)
5331 cl.CloseIssue()
5332 props = cl.GetIssueProperties()
5333 patch_num = len(props['patchsets'])
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005334 comment = "Committed patchset #%d (id:%d) manually as %s" % (
5335 patch_num, props['patchsets'][-1], revision)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005336 if options.bypass_hooks:
5337 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
5338 else:
5339 comment += ' (presubmit successful).'
5340 cl.RpcServer().add_comment(cl.GetIssue(), comment)
5341
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005342 if os.path.isfile(POSTUPSTREAM_HOOK):
5343 RunCommand([POSTUPSTREAM_HOOK, merge_base], error_ok=True)
5344
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005345 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005346
5347
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005348def PushToGitWithAutoRebase(remote, branch, original_description,
5349 git_numberer_enabled, max_attempts=3):
5350 """Pushes current HEAD commit on top of remote's branch.
5351
5352 Attempts to fetch and autorebase on push failures.
5353 Adds git number footers on the fly.
5354
5355 Returns integer code from last command.
5356 """
5357 cherry = RunGit(['rev-parse', 'HEAD']).strip()
5358 code = 0
5359 attempts_left = max_attempts
5360 while attempts_left:
5361 attempts_left -= 1
5362 print('Attempt %d of %d' % (max_attempts - attempts_left, max_attempts))
5363
5364 # Fetch remote/branch into local cherry_pick_branch, overriding the latter.
5365 # If fetch fails, retry.
5366 print('Fetching %s/%s...' % (remote, branch))
5367 code, out = RunGitWithCode(
5368 ['retry', 'fetch', remote,
5369 '+%s:refs/heads/%s' % (branch, CHERRY_PICK_BRANCH)])
5370 if code:
5371 print('Fetch failed with exit code %d.' % code)
5372 print(out.strip())
5373 continue
5374
5375 print('Cherry-picking commit on top of latest %s' % branch)
5376 RunGitWithCode(['checkout', 'refs/heads/%s' % CHERRY_PICK_BRANCH],
5377 suppress_stderr=True)
5378 parent_hash = RunGit(['rev-parse', 'HEAD']).strip()
5379 code, out = RunGitWithCode(['cherry-pick', cherry])
5380 if code:
5381 print('Your patch doesn\'t apply cleanly to \'%s\' HEAD @ %s, '
5382 'the following files have merge conflicts:' %
5383 (branch, parent_hash))
Aaron Gable7817f022017-12-12 09:43:17 -08005384 print(RunGit(['-c', 'core.quotePath=false', 'diff',
5385 '--name-status', '--diff-filter=U']).strip())
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005386 print('Please rebase your patch and try again.')
5387 RunGitWithCode(['cherry-pick', '--abort'])
5388 break
5389
5390 commit_desc = ChangeDescription(original_description)
5391 if git_numberer_enabled:
5392 logging.debug('Adding git number footers')
5393 parent_msg = RunGit(['show', '-s', '--format=%B', parent_hash]).strip()
5394 commit_desc.update_with_git_number_footers(parent_hash, parent_msg,
5395 branch)
5396 # Ensure timestamps are monotonically increasing.
5397 timestamp = max(1 + _get_committer_timestamp(parent_hash),
5398 _get_committer_timestamp('HEAD'))
5399 _git_amend_head(commit_desc.description, timestamp)
5400
5401 code, out = RunGitWithCode(
5402 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
5403 print(out)
5404 if code == 0:
5405 break
5406 if IsFatalPushFailure(out):
5407 print('Fatal push error. Make sure your .netrc credentials and git '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02005408 'user.email are correct and you have push access to the repo.\n'
5409 'Hint: run command below to diangose common Git/Gerrit credential '
5410 'problems:\n'
5411 ' git cl creds-check\n')
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005412 break
5413 return code
5414
5415
5416def IsFatalPushFailure(push_stdout):
5417 """True if retrying push won't help."""
5418 return '(prohibited by Gerrit)' in push_stdout
5419
5420
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005421@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005422def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00005423 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005424 parser.add_option('-b', dest='newbranch',
5425 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005426 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07005427 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005428 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07005429 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005430 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005431 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00005432 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005433 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005434 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005435 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005436
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005437
5438 group = optparse.OptionGroup(
5439 parser,
5440 'Options for continuing work on the current issue uploaded from a '
5441 'different clone (e.g. different machine). Must be used independently '
5442 'from the other options. No issue number should be specified, and the '
5443 'branch must have an issue number associated with it')
5444 group.add_option('--reapply', action='store_true', dest='reapply',
5445 help='Reset the branch and reapply the issue.\n'
5446 'CAUTION: This will undo any local changes in this '
5447 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005448
5449 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005450 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005451 parser.add_option_group(group)
5452
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005453 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005454 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005455 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005456 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005457 auth_config = auth.extract_auth_config_from_options(options)
5458
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005459 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005460 if options.newbranch:
5461 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005462 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005463 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005464
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005465 cl = Changelist(auth_config=auth_config,
5466 codereview=options.forced_codereview)
5467 if not cl.GetIssue():
5468 parser.error('current branch must have an associated issue')
5469
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005470 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005471 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005472 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005473
5474 RunGit(['reset', '--hard', upstream])
5475 if options.pull:
5476 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005477
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005478 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
5479 options.directory)
5480
5481 if len(args) != 1 or not args[0]:
5482 parser.error('Must specify issue number or url')
5483
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005484 target_issue_arg = ParseIssueNumberArgument(args[0],
5485 options.forced_codereview)
5486 if not target_issue_arg.valid:
5487 parser.error('invalid codereview url or CL id')
5488
5489 cl_kwargs = {
5490 'auth_config': auth_config,
5491 'codereview_host': target_issue_arg.hostname,
5492 'codereview': options.forced_codereview,
5493 }
5494 detected_codereview_from_url = False
5495 if target_issue_arg.codereview and not options.forced_codereview:
5496 detected_codereview_from_url = True
5497 cl_kwargs['codereview'] = target_issue_arg.codereview
5498 cl_kwargs['issue'] = target_issue_arg.issue
5499
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005500 # We don't want uncommitted changes mixed up with the patch.
5501 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005502 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005503
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005504 if options.newbranch:
5505 if options.force:
5506 RunGit(['branch', '-D', options.newbranch],
5507 stderr=subprocess2.PIPE, error_ok=True)
5508 RunGit(['new-branch', options.newbranch])
5509
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005510 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005511
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005512 if cl.IsGerrit():
5513 if options.reject:
5514 parser.error('--reject is not supported with Gerrit codereview.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005515 if options.directory:
5516 parser.error('--directory is not supported with Gerrit codereview.')
5517
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005518 if detected_codereview_from_url:
5519 print('canonical issue/change URL: %s (type: %s)\n' %
5520 (cl.GetIssueURL(), target_issue_arg.codereview))
5521
5522 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07005523 options.nocommit, options.directory,
5524 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005525
5526
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005527def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005528 """Fetches the tree status and returns either 'open', 'closed',
5529 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005530 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005531 if url:
5532 status = urllib2.urlopen(url).read().lower()
5533 if status.find('closed') != -1 or status == '0':
5534 return 'closed'
5535 elif status.find('open') != -1 or status == '1':
5536 return 'open'
5537 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005538 return 'unset'
5539
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005540
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005541def GetTreeStatusReason():
5542 """Fetches the tree status from a json url and returns the message
5543 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005544 url = settings.GetTreeStatusUrl()
5545 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005546 connection = urllib2.urlopen(json_url)
5547 status = json.loads(connection.read())
5548 connection.close()
5549 return status['message']
5550
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005551
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005552def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005553 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005554 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005555 status = GetTreeStatus()
5556 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005557 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005558 return 2
5559
vapiera7fbd5a2016-06-16 09:17:49 -07005560 print('The tree is %s' % status)
5561 print()
5562 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005563 if status != 'open':
5564 return 1
5565 return 0
5566
5567
maruel@chromium.org15192402012-09-06 12:38:29 +00005568def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005569 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005570 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005571 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005572 '-b', '--bot', action='append',
5573 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5574 'times to specify multiple builders. ex: '
5575 '"-b win_rel -b win_layout". See '
5576 'the try server waterfall for the builders name and the tests '
5577 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005578 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005579 '-B', '--bucket', default='',
5580 help=('Buildbucket bucket to send the try requests.'))
5581 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005582 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005583 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005584 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005585 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005586 help='Revision to use for the try job; default: the revision will '
5587 'be determined by the try recipe that builder runs, which usually '
5588 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005589 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005590 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005591 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005592 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005593 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005594 '--project',
5595 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005596 'in recipe to determine to which repository or directory to '
5597 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005598 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005599 '-p', '--property', dest='properties', action='append', default=[],
5600 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005601 'key2=value2 etc. The value will be treated as '
5602 'json if decodable, or as string otherwise. '
5603 'NOTE: using this may make your try job not usable for CQ, '
5604 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005605 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005606 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5607 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005608 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005609 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09005610 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005611 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09005612 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005613 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005614
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005615 if options.master and options.master.startswith('luci.'):
5616 parser.error(
5617 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005618 # Make sure that all properties are prop=value pairs.
5619 bad_params = [x for x in options.properties if '=' not in x]
5620 if bad_params:
5621 parser.error('Got properties with missing "=": %s' % bad_params)
5622
maruel@chromium.org15192402012-09-06 12:38:29 +00005623 if args:
5624 parser.error('Unknown arguments: %s' % args)
5625
Koji Ishii31c14782018-01-08 17:17:33 +09005626 cl = Changelist(auth_config=auth_config, issue=options.issue,
5627 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00005628 if not cl.GetIssue():
5629 parser.error('Need to upload first')
5630
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005631 if cl.IsGerrit():
5632 # HACK: warm up Gerrit change detail cache to save on RPCs.
5633 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5634
tandriie113dfd2016-10-11 10:20:12 -07005635 error_message = cl.CannotTriggerTryJobReason()
5636 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005637 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005638
borenet6c0efe62016-10-19 08:13:29 -07005639 if options.bucket and options.master:
5640 parser.error('Only one of --bucket and --master may be used.')
5641
qyearsley1fdfcb62016-10-24 13:22:03 -07005642 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005643
qyearsleydd49f942016-10-28 11:57:22 -07005644 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5645 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005646 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005647 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005648 print('git cl try with no bots now defaults to CQ dry run.')
5649 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5650 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005651
borenet6c0efe62016-10-19 08:13:29 -07005652 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005653 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005654 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005655 'of bot requires an initial job from a parent (usually a builder). '
5656 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005657 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005658 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005659
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005660 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005661 # TODO(tandrii): Checking local patchset against remote patchset is only
5662 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5663 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07005664 print('Warning: Codereview server has newer patchsets (%s) than most '
5665 'recent upload from local checkout (%s). Did a previous upload '
5666 'fail?\n'
5667 'By default, git cl try uses the latest patchset from '
5668 'codereview, continuing to use patchset %s.\n' %
5669 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07005670
tandrii568043b2016-10-11 07:49:18 -07005671 try:
borenet6c0efe62016-10-19 08:13:29 -07005672 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
5673 patchset)
tandrii568043b2016-10-11 07:49:18 -07005674 except BuildbucketResponseException as ex:
5675 print('ERROR: %s' % ex)
5676 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005677 return 0
5678
5679
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005680def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005681 """Prints info about try jobs associated with current CL."""
5682 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005683 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005684 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005685 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005686 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005687 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005688 '--color', action='store_true', default=setup_color.IS_TTY,
5689 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005690 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005691 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5692 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005693 group.add_option(
Stefan Zager1306bd02017-06-22 19:26:46 -07005694 '--json', help=('Path of JSON output file to write try job results to,'
5695 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005696 parser.add_option_group(group)
5697 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07005698 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005699 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07005700 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005701 if args:
5702 parser.error('Unrecognized args: %s' % ' '.join(args))
5703
5704 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07005705 cl = Changelist(
5706 issue=options.issue, codereview=options.forced_codereview,
5707 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005708 if not cl.GetIssue():
5709 parser.error('Need to upload first')
5710
tandrii221ab252016-10-06 08:12:04 -07005711 patchset = options.patchset
5712 if not patchset:
5713 patchset = cl.GetMostRecentPatchset()
5714 if not patchset:
5715 parser.error('Codereview doesn\'t know about issue %s. '
5716 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005717 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005718 cl.GetIssue())
5719
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005720 # TODO(tandrii): Checking local patchset against remote patchset is only
5721 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5722 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07005723 print('Warning: Codereview server has newer patchsets (%s) than most '
5724 'recent upload from local checkout (%s). Did a previous upload '
5725 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07005726 'By default, git cl try-results uses the latest patchset from '
5727 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07005728 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005729 try:
tandrii221ab252016-10-06 08:12:04 -07005730 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005731 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005732 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005733 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005734 if options.json:
5735 write_try_results_json(options.json, jobs)
5736 else:
5737 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005738 return 0
5739
5740
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005741@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005742def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005743 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005744 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005745 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005746 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005747
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005748 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005749 if args:
5750 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005751 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005752 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005753 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005754 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005755
5756 # Clear configured merge-base, if there is one.
5757 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005758 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005759 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005760 return 0
5761
5762
thestig@chromium.org00858c82013-12-02 23:08:03 +00005763def CMDweb(parser, args):
5764 """Opens the current CL in the web browser."""
5765 _, args = parser.parse_args(args)
5766 if args:
5767 parser.error('Unrecognized args: %s' % ' '.join(args))
5768
5769 issue_url = Changelist().GetIssueURL()
5770 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005771 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005772 return 1
5773
5774 webbrowser.open(issue_url)
5775 return 0
5776
5777
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005778def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005779 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005780 parser.add_option('-d', '--dry-run', action='store_true',
5781 help='trigger in dry run mode')
5782 parser.add_option('-c', '--clear', action='store_true',
5783 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005784 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005785 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005786 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005787 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005788 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005789 if args:
5790 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005791 if options.dry_run and options.clear:
5792 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5793
iannuccie53c9352016-08-17 14:40:40 -07005794 cl = Changelist(auth_config=auth_config, issue=options.issue,
5795 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005796 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005797 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005798 elif options.dry_run:
5799 state = _CQState.DRY_RUN
5800 else:
5801 state = _CQState.COMMIT
5802 if not cl.GetIssue():
5803 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005804 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005805 return 0
5806
5807
groby@chromium.org411034a2013-02-26 15:12:01 +00005808def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005809 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005810 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005811 auth.add_auth_options(parser)
5812 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005813 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005814 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005815 if args:
5816 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005817 cl = Changelist(auth_config=auth_config, issue=options.issue,
5818 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005819 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005820 if not cl.GetIssue():
5821 DieWithError('ERROR No issue to close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005822 cl.CloseIssue()
5823 return 0
5824
5825
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005826def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005827 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005828 parser.add_option(
5829 '--stat',
5830 action='store_true',
5831 dest='stat',
5832 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005833 auth.add_auth_options(parser)
5834 options, args = parser.parse_args(args)
5835 auth_config = auth.extract_auth_config_from_options(options)
5836 if args:
5837 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005838
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005839 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005840 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005841 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005842 if not issue:
5843 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005844
Aaron Gablea718c3e2017-08-28 17:47:28 -07005845 base = cl._GitGetBranchConfigValue('last-upload-hash')
5846 if not base:
5847 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5848 if not base:
5849 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5850 revision_info = detail['revisions'][detail['current_revision']]
5851 fetch_info = revision_info['fetch']['http']
5852 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5853 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005854
Aaron Gablea718c3e2017-08-28 17:47:28 -07005855 cmd = ['git', 'diff']
5856 if options.stat:
5857 cmd.append('--stat')
5858 cmd.append(base)
5859 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005860
5861 return 0
5862
5863
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005864def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005865 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005866 parser.add_option(
5867 '--no-color',
5868 action='store_true',
5869 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005870 parser.add_option(
5871 '--batch',
5872 action='store_true',
5873 help='Do not run interactively, just suggest some')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005874 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005875 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005876 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005877
5878 author = RunGit(['config', 'user.email']).strip() or None
5879
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005880 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005881
5882 if args:
5883 if len(args) > 1:
5884 parser.error('Unknown args')
5885 base_branch = args[0]
5886 else:
5887 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005888 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005889
5890 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005891 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5892
5893 if options.batch:
5894 db = owners.Database(change.RepositoryRoot(), file, os.path)
5895 print('\n'.join(db.reviewers_for(affected_files, author)))
5896 return 0
5897
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005898 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005899 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005900 change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02005901 author, fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005902 disable_color=options.no_color,
5903 override_files=change.OriginalOwnersFiles()).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005904
5905
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005906def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005907 """Generates a diff command."""
5908 # Generate diff for the current branch's changes.
Aaron Gablef4068aa2017-12-12 15:14:09 -08005909 diff_cmd = ['-c', 'core.quotePath=false', 'diff',
5910 '--no-ext-diff', '--no-prefix', diff_type,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005911 upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005912
5913 if args:
5914 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005915 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005916 diff_cmd.append(arg)
5917 else:
5918 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005919
5920 return diff_cmd
5921
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005922
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005923def MatchingFileType(file_name, extensions):
5924 """Returns true if the file name ends with one of the given extensions."""
5925 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005926
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005927
enne@chromium.org555cfe42014-01-29 18:21:39 +00005928@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005929def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005930 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005931 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005932 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005933 parser.add_option('--full', action='store_true',
5934 help='Reformat the full content of all touched files')
5935 parser.add_option('--dry-run', action='store_true',
5936 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005937 parser.add_option('--python', action='store_true',
5938 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005939 parser.add_option('--js', action='store_true',
5940 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005941 parser.add_option('--diff', action='store_true',
5942 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005943 parser.add_option('--presubmit', action='store_true',
5944 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005945 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005946
Daniel Chengc55eecf2016-12-30 03:11:02 -08005947 # Normalize any remaining args against the current path, so paths relative to
5948 # the current directory are still resolved as expected.
5949 args = [os.path.join(os.getcwd(), arg) for arg in args]
5950
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005951 # git diff generates paths against the root of the repository. Change
5952 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005953 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005954 if rel_base_path:
5955 os.chdir(rel_base_path)
5956
digit@chromium.org29e47272013-05-17 17:01:46 +00005957 # Grab the merge-base commit, i.e. the upstream commit of the current
5958 # branch when it was created or the last time it was rebased. This is
5959 # to cover the case where the user may have called "git fetch origin",
5960 # moving the origin branch to a newer commit, but hasn't rebased yet.
5961 upstream_commit = None
5962 cl = Changelist()
5963 upstream_branch = cl.GetUpstreamBranch()
5964 if upstream_branch:
5965 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5966 upstream_commit = upstream_commit.strip()
5967
5968 if not upstream_commit:
5969 DieWithError('Could not find base commit for this branch. '
5970 'Are you in detached state?')
5971
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005972 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5973 diff_output = RunGit(changed_files_cmd)
5974 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005975 # Filter out files deleted by this CL
5976 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005977
Christopher Lamc5ba6922017-01-24 11:19:14 +11005978 if opts.js:
5979 CLANG_EXTS.append('.js')
5980
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005981 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5982 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5983 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005984 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005985
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005986 top_dir = os.path.normpath(
5987 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5988
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005989 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5990 # formatted. This is used to block during the presubmit.
5991 return_value = 0
5992
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005993 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005994 # Locate the clang-format binary in the checkout
5995 try:
5996 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005997 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005998 DieWithError(e)
5999
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006000 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006001 cmd = [clang_format_tool]
6002 if not opts.dry_run and not opts.diff:
6003 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006004 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006005 if opts.diff:
6006 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006007 else:
6008 env = os.environ.copy()
6009 env['PATH'] = str(os.path.dirname(clang_format_tool))
6010 try:
6011 script = clang_format.FindClangFormatScriptInChromiumTree(
6012 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07006013 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006014 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00006015
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006016 cmd = [sys.executable, script, '-p0']
6017 if not opts.dry_run and not opts.diff:
6018 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00006019
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006020 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
6021 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006022
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006023 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
6024 if opts.diff:
6025 sys.stdout.write(stdout)
6026 if opts.dry_run and len(stdout) > 0:
6027 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006028
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006029 # Similar code to above, but using yapf on .py files rather than clang-format
6030 # on C/C++ files
6031 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006032 yapf_tool = gclient_utils.FindExecutable('yapf')
6033 if yapf_tool is None:
6034 DieWithError('yapf not found in PATH')
6035
6036 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006037 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006038 cmd = [yapf_tool]
6039 if not opts.dry_run and not opts.diff:
6040 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006041 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006042 if opts.diff:
6043 sys.stdout.write(stdout)
6044 else:
6045 # TODO(sbc): yapf --lines mode still has some issues.
6046 # https://github.com/google/yapf/issues/154
6047 DieWithError('--python currently only works with --full')
6048
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006049 # Dart's formatter does not have the nice property of only operating on
6050 # modified chunks, so hard code full.
6051 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006052 try:
6053 command = [dart_format.FindDartFmtToolInChromiumTree()]
6054 if not opts.dry_run and not opts.diff:
6055 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006056 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006057
ppi@chromium.org6593d932016-03-03 15:41:15 +00006058 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006059 if opts.dry_run and stdout:
6060 return_value = 2
6061 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07006062 print('Warning: Unable to check Dart code formatting. Dart SDK not '
6063 'found in this checkout. Files in other languages are still '
6064 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006065
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006066 # Format GN build files. Always run on full build files for canonical form.
6067 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01006068 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07006069 if opts.dry_run or opts.diff:
6070 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006071 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07006072 gn_ret = subprocess2.call(cmd + [gn_diff_file],
6073 shell=sys.platform == 'win32',
6074 cwd=top_dir)
6075 if opts.dry_run and gn_ret == 2:
6076 return_value = 2 # Not formatted.
6077 elif opts.diff and gn_ret == 2:
6078 # TODO this should compute and print the actual diff.
6079 print("This change has GN build file diff for " + gn_diff_file)
6080 elif gn_ret != 0:
6081 # For non-dry run cases (and non-2 return values for dry-run), a
6082 # nonzero error code indicates a failure, probably because the file
6083 # doesn't parse.
6084 DieWithError("gn format failed on " + gn_diff_file +
6085 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006086
Ilya Shermane081cbe2017-08-15 17:51:04 -07006087 # Skip the metrics formatting from the global presubmit hook. These files have
6088 # a separate presubmit hook that issues an error if the files need formatting,
6089 # whereas the top-level presubmit script merely issues a warning. Formatting
6090 # these files is somewhat slow, so it's important not to duplicate the work.
6091 if not opts.presubmit:
6092 for xml_dir in GetDirtyMetricsDirs(diff_files):
6093 tool_dir = os.path.join(top_dir, xml_dir)
6094 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
6095 if opts.dry_run or opts.diff:
6096 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07006097 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07006098 if opts.diff:
6099 sys.stdout.write(stdout)
6100 if opts.dry_run and stdout:
6101 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05006102
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006103 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006104
Steven Holte2e664bf2017-04-21 13:10:47 -07006105def GetDirtyMetricsDirs(diff_files):
6106 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
6107 metrics_xml_dirs = [
6108 os.path.join('tools', 'metrics', 'actions'),
6109 os.path.join('tools', 'metrics', 'histograms'),
6110 os.path.join('tools', 'metrics', 'rappor'),
6111 os.path.join('tools', 'metrics', 'ukm')]
6112 for xml_dir in metrics_xml_dirs:
6113 if any(file.startswith(xml_dir) for file in xml_diff_files):
6114 yield xml_dir
6115
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006116
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006117@subcommand.usage('<codereview url or issue id>')
6118def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006119 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006120 _, args = parser.parse_args(args)
6121
6122 if len(args) != 1:
6123 parser.print_help()
6124 return 1
6125
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00006126 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00006127 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02006128 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02006129
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00006130 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006131
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006132 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00006133 output = RunGit(['config', '--local', '--get-regexp',
6134 r'branch\..*\.%s' % issueprefix],
6135 error_ok=True)
6136 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006137 if issue == target_issue:
6138 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006139
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006140 branches = []
6141 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07006142 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006143 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07006144 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006145 return 1
6146 if len(branches) == 1:
6147 RunGit(['checkout', branches[0]])
6148 else:
vapiera7fbd5a2016-06-16 09:17:49 -07006149 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006150 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07006151 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006152 which = raw_input('Choose by index: ')
6153 try:
6154 RunGit(['checkout', branches[int(which)]])
6155 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07006156 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006157 return 1
6158
6159 return 0
6160
6161
maruel@chromium.org29404b52014-09-08 22:58:00 +00006162def CMDlol(parser, args):
6163 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07006164 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00006165 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
6166 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
6167 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07006168 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00006169 return 0
6170
6171
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006172class OptionParser(optparse.OptionParser):
6173 """Creates the option parse and add --verbose support."""
6174 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006175 optparse.OptionParser.__init__(
6176 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006177 self.add_option(
6178 '-v', '--verbose', action='count', default=0,
6179 help='Use 2 times for more debugging info')
6180
6181 def parse_args(self, args=None, values=None):
6182 options, args = optparse.OptionParser.parse_args(self, args, values)
6183 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01006184 logging.basicConfig(
6185 level=levels[min(options.verbose, len(levels) - 1)],
6186 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
6187 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006188 return options, args
6189
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006190
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006191def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006192 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07006193 print('\nYour python version %s is unsupported, please upgrade.\n' %
6194 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006195 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006196
maruel@chromium.orgddd59412011-11-30 14:20:38 +00006197 # Reload settings.
6198 global settings
6199 settings = Settings()
6200
maruel@chromium.org39c0b222013-08-17 16:57:01 +00006201 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006202 dispatcher = subcommand.CommandDispatcher(__name__)
6203 try:
6204 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00006205 except auth.AuthenticationError as e:
6206 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07006207 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006208 if e.code != 500:
6209 raise
6210 DieWithError(
6211 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
6212 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00006213 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006214
6215
6216if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006217 # These affect sys.stdout so do it outside of main() to simplify mocks in
6218 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00006219 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00006220 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00006221 try:
6222 sys.exit(main(sys.argv[1:]))
6223 except KeyboardInterrupt:
6224 sys.stderr.write('interrupted\n')
6225 sys.exit(1)