blob: 07cf9f37b1ddb28f1ab4637f0600380383dd36be [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:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001585 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001586
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001587 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1588 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001589 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1590 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001591 else:
1592 # Assume url.
1593 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1594 urlparse.urlparse(issue_arg))
1595 if not parsed_issue_arg or not parsed_issue_arg.valid:
1596 DieWithError('Failed to parse issue argument "%s". '
1597 'Must be an issue number or a valid URL.' % issue_arg)
1598 return self._codereview_impl.CMDPatchWithParsedIssue(
Aaron Gable62619a32017-06-16 08:22:09 -07001599 parsed_issue_arg, reject, nocommit, directory, False)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001600
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001601 def CMDUpload(self, options, git_diff_args, orig_args):
1602 """Uploads a change to codereview."""
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001603 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001604 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001605 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001606 else:
1607 if self.GetBranch() is None:
1608 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1609
1610 # Default to diffing against common ancestor of upstream branch
1611 base_branch = self.GetCommonAncestorWithUpstream()
1612 git_diff_args = [base_branch, 'HEAD']
1613
Aaron Gablec4c40d12017-05-22 11:49:53 -07001614 # Warn about Rietveld deprecation for initial uploads to Rietveld.
1615 if not self.IsGerrit() and not self.GetIssue():
1616 print('=====================================')
1617 print('NOTICE: Rietveld is being deprecated. '
1618 'You can upload changes to Gerrit with')
1619 print(' git cl upload --gerrit')
1620 print('or set Gerrit to be your default code review tool with')
1621 print(' git config gerrit.host true')
1622 print('=====================================')
1623
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001624 # Fast best-effort checks to abort before running potentially
1625 # expensive hooks if uploading is likely to fail anyway. Passing these
1626 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001627 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001628 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001629
1630 # Apply watchlists on upload.
1631 change = self.GetChange(base_branch, None)
1632 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1633 files = [f.LocalPath() for f in change.AffectedFiles()]
1634 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001635 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001636
1637 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001638 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001639 # Set the reviewer list now so that presubmit checks can access it.
1640 change_description = ChangeDescription(change.FullDescriptionText())
1641 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001642 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001643 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001644 change)
1645 change.SetDescriptionText(change_description.description)
1646 hook_results = self.RunHook(committing=False,
1647 may_prompt=not options.force,
1648 verbose=options.verbose,
1649 change=change)
1650 if not hook_results.should_continue():
1651 return 1
1652 if not options.reviewers and hook_results.reviewers:
1653 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001654 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001655
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001656 # TODO(tandrii): Checking local patchset against remote patchset is only
1657 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1658 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001659 latest_patchset = self.GetMostRecentPatchset()
1660 local_patchset = self.GetPatchset()
1661 if (latest_patchset and local_patchset and
1662 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001663 print('The last upload made from this repository was patchset #%d but '
1664 'the most recent patchset on the server is #%d.'
1665 % (local_patchset, latest_patchset))
1666 print('Uploading will still work, but if you\'ve uploaded to this '
1667 'issue from another machine or branch the patch you\'re '
1668 'uploading now might not include those changes.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001669 confirm_or_exit(action='upload')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001670
1671 print_stats(options.similarity, options.find_copies, git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001672 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001673 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001674 if options.use_commit_queue:
1675 self.SetCQState(_CQState.COMMIT)
1676 elif options.cq_dry_run:
1677 self.SetCQState(_CQState.DRY_RUN)
1678
tandrii5d48c322016-08-18 16:19:37 -07001679 _git_set_branch_config_value('last-upload-hash',
1680 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001681 # Run post upload hooks, if specified.
1682 if settings.GetRunPostUploadHook():
1683 presubmit_support.DoPostUploadExecuter(
1684 change,
1685 self,
1686 settings.GetRoot(),
1687 options.verbose,
1688 sys.stdout)
1689
1690 # Upload all dependencies if specified.
1691 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001692 print()
1693 print('--dependencies has been specified.')
1694 print('All dependent local branches will be re-uploaded.')
1695 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001696 # Remove the dependencies flag from args so that we do not end up in a
1697 # loop.
1698 orig_args.remove('--dependencies')
1699 ret = upload_branch_deps(self, orig_args)
1700 return ret
1701
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001702 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001703 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001704
1705 Issue must have been already uploaded and known.
1706 """
1707 assert new_state in _CQState.ALL_STATES
1708 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001709 try:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001710 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 13:22:03 -07001711 return 0
1712 except KeyboardInterrupt:
1713 raise
1714 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001715 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001716 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001717 ' * Your project has no CQ,\n'
1718 ' * You don\'t have permission to change the CQ state,\n'
1719 ' * There\'s a bug in this code (see stack trace below).\n'
1720 'Consider specifying which bots to trigger manually or asking your '
1721 'project owners for permissions or contacting Chrome Infra at:\n'
1722 'https://www.chromium.org/infra\n\n' %
1723 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001724 # Still raise exception so that stack trace is printed.
1725 raise
1726
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001727 # Forward methods to codereview specific implementation.
1728
Aaron Gable636b13f2017-07-14 10:42:48 -07001729 def AddComment(self, message, publish=None):
1730 return self._codereview_impl.AddComment(message, publish=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001731
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001732 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001733 """Returns list of _CommentSummary for each comment.
1734
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001735 args:
1736 readable: determines whether the output is designed for a human or a machine
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001737 """
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001738 return self._codereview_impl.GetCommentsSummary(readable)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001739
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001740 def CloseIssue(self):
1741 return self._codereview_impl.CloseIssue()
1742
1743 def GetStatus(self):
1744 return self._codereview_impl.GetStatus()
1745
1746 def GetCodereviewServer(self):
1747 return self._codereview_impl.GetCodereviewServer()
1748
tandriide281ae2016-10-12 06:02:30 -07001749 def GetIssueOwner(self):
1750 """Get owner from codereview, which may differ from this checkout."""
1751 return self._codereview_impl.GetIssueOwner()
1752
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001753 def GetMostRecentPatchset(self):
1754 return self._codereview_impl.GetMostRecentPatchset()
1755
tandriide281ae2016-10-12 06:02:30 -07001756 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001757 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriide281ae2016-10-12 06:02:30 -07001758 return self._codereview_impl.CannotTriggerTryJobReason()
1759
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001760 def GetTryJobProperties(self, patchset=None):
1761 """Returns dictionary of properties to launch try job."""
1762 return self._codereview_impl.GetTryJobProperties(patchset=patchset)
tandrii8c5a3532016-11-04 07:52:02 -07001763
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001764 def __getattr__(self, attr):
1765 # This is because lots of untested code accesses Rietveld-specific stuff
1766 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001767 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001768 # Note that child method defines __getattr__ as well, and forwards it here,
1769 # because _RietveldChangelistImpl is not cleaned up yet, and given
1770 # deprecation of Rietveld, it should probably be just removed.
1771 # Until that time, avoid infinite recursion by bypassing __getattr__
1772 # of implementation class.
1773 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001774
1775
1776class _ChangelistCodereviewBase(object):
1777 """Abstract base class encapsulating codereview specifics of a changelist."""
1778 def __init__(self, changelist):
1779 self._changelist = changelist # instance of Changelist
1780
1781 def __getattr__(self, attr):
1782 # Forward methods to changelist.
1783 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1784 # _RietveldChangelistImpl to avoid this hack?
1785 return getattr(self._changelist, attr)
1786
1787 def GetStatus(self):
1788 """Apply a rough heuristic to give a simple summary of an issue's review
1789 or CQ status, assuming adherence to a common workflow.
1790
1791 Returns None if no issue for this branch, or specific string keywords.
1792 """
1793 raise NotImplementedError()
1794
1795 def GetCodereviewServer(self):
1796 """Returns server URL without end slash, like "https://codereview.com"."""
1797 raise NotImplementedError()
1798
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001799 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001800 """Fetches and returns description from the codereview server."""
1801 raise NotImplementedError()
1802
tandrii5d48c322016-08-18 16:19:37 -07001803 @classmethod
1804 def IssueConfigKey(cls):
1805 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001806 raise NotImplementedError()
1807
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001808 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001809 def PatchsetConfigKey(cls):
1810 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001811 raise NotImplementedError()
1812
tandrii5d48c322016-08-18 16:19:37 -07001813 @classmethod
1814 def CodereviewServerConfigKey(cls):
1815 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001816 raise NotImplementedError()
1817
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001818 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001819 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001820 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001821
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001822 def GetRietveldObjForPresubmit(self):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001823 # This is an unfortunate Rietveld-embeddedness in presubmit.
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001824 # For non-Rietveld code reviews, this probably should return a dummy object.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001825 raise NotImplementedError()
1826
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001827 def GetGerritObjForPresubmit(self):
1828 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1829 return None
1830
dsansomee2d6fd92016-09-08 00:10:47 -07001831 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001832 """Update the description on codereview site."""
1833 raise NotImplementedError()
1834
Aaron Gable636b13f2017-07-14 10:42:48 -07001835 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001836 """Posts a comment to the codereview site."""
1837 raise NotImplementedError()
1838
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001839 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001840 raise NotImplementedError()
1841
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001842 def CloseIssue(self):
1843 """Closes the issue."""
1844 raise NotImplementedError()
1845
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001846 def GetMostRecentPatchset(self):
1847 """Returns the most recent patchset number from the codereview site."""
1848 raise NotImplementedError()
1849
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001850 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07001851 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001852 """Fetches and applies the issue.
1853
1854 Arguments:
1855 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1856 reject: if True, reject the failed patch instead of switching to 3-way
1857 merge. Rietveld only.
1858 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1859 only.
1860 directory: switch to directory before applying the patch. Rietveld only.
Aaron Gable62619a32017-06-16 08:22:09 -07001861 force: if true, overwrites existing local state.
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001862 """
1863 raise NotImplementedError()
1864
1865 @staticmethod
1866 def ParseIssueURL(parsed_url):
1867 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1868 failed."""
1869 raise NotImplementedError()
1870
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001871 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001872 """Best effort check that user is authenticated with codereview server.
1873
1874 Arguments:
1875 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001876 refresh: whether to attempt to refresh credentials. Ignored if not
1877 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001878 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001879 raise NotImplementedError()
1880
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001881 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001882 """Best effort check that uploading isn't supposed to fail for predictable
1883 reasons.
1884
1885 This method should raise informative exception if uploading shouldn't
1886 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001887
1888 Arguments:
1889 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001890 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001891 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001892
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001893 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001894 """Uploads a change to codereview."""
1895 raise NotImplementedError()
1896
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001897 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001898 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001899
1900 Issue must have been already uploaded and known.
1901 """
1902 raise NotImplementedError()
1903
tandriie113dfd2016-10-11 10:20:12 -07001904 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001905 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriie113dfd2016-10-11 10:20:12 -07001906 raise NotImplementedError()
1907
tandriide281ae2016-10-12 06:02:30 -07001908 def GetIssueOwner(self):
1909 raise NotImplementedError()
1910
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001911 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001912 raise NotImplementedError()
1913
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001914
1915class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001916
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001917 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001918 super(_RietveldChangelistImpl, self).__init__(changelist)
1919 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001920 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001921 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001922
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001923 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001924 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001925 self._props = None
1926 self._rpc_server = None
1927
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001928 def GetCodereviewServer(self):
1929 if not self._rietveld_server:
1930 # If we're on a branch then get the server potentially associated
1931 # with that branch.
1932 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001933 self._rietveld_server = gclient_utils.UpgradeToHttps(
1934 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001935 if not self._rietveld_server:
1936 self._rietveld_server = settings.GetDefaultServerUrl()
1937 return self._rietveld_server
1938
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001939 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001940 """Best effort check that user is authenticated with Rietveld server."""
1941 if self._auth_config.use_oauth2:
1942 authenticator = auth.get_authenticator_for_host(
1943 self.GetCodereviewServer(), self._auth_config)
1944 if not authenticator.has_cached_credentials():
1945 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001946 if refresh:
1947 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001948
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001949 def EnsureCanUploadPatchset(self, force):
1950 # No checks for Rietveld because we are deprecating Rietveld.
1951 pass
1952
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001953 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001954 issue = self.GetIssue()
1955 assert issue
1956 try:
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001957 return self.RpcServer().get_description(issue, force=force).strip()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001958 except urllib2.HTTPError as e:
1959 if e.code == 404:
1960 DieWithError(
1961 ('\nWhile fetching the description for issue %d, received a '
1962 '404 (not found)\n'
1963 'error. It is likely that you deleted this '
1964 'issue on the server. If this is the\n'
1965 'case, please run\n\n'
1966 ' git cl issue 0\n\n'
1967 'to clear the association with the deleted issue. Then run '
1968 'this command again.') % issue)
1969 else:
1970 DieWithError(
1971 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1972 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001973 print('Warning: Failed to retrieve CL description due to network '
1974 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001975 return ''
1976
1977 def GetMostRecentPatchset(self):
1978 return self.GetIssueProperties()['patchsets'][-1]
1979
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001980 def GetIssueProperties(self):
1981 if self._props is None:
1982 issue = self.GetIssue()
1983 if not issue:
1984 self._props = {}
1985 else:
1986 self._props = self.RpcServer().get_issue_properties(issue, True)
1987 return self._props
1988
tandriie113dfd2016-10-11 10:20:12 -07001989 def CannotTriggerTryJobReason(self):
1990 props = self.GetIssueProperties()
1991 if not props:
1992 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1993 if props.get('closed'):
1994 return 'CL %s is closed' % self.GetIssue()
1995 if props.get('private'):
1996 return 'CL %s is private' % self.GetIssue()
1997 return None
1998
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001999 def GetTryJobProperties(self, patchset=None):
2000 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07002001 project = (self.GetIssueProperties() or {}).get('project')
2002 return {
2003 'issue': self.GetIssue(),
2004 'patch_project': project,
2005 'patch_storage': 'rietveld',
2006 'patchset': patchset or self.GetPatchset(),
2007 'rietveld': self.GetCodereviewServer(),
2008 }
2009
tandriide281ae2016-10-12 06:02:30 -07002010 def GetIssueOwner(self):
2011 return (self.GetIssueProperties() or {}).get('owner_email')
2012
Aaron Gable636b13f2017-07-14 10:42:48 -07002013 def AddComment(self, message, publish=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002014 return self.RpcServer().add_comment(self.GetIssue(), message)
2015
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002016 def GetCommentsSummary(self, _readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002017 summary = []
2018 for message in self.GetIssueProperties().get('messages', []):
2019 date = datetime.datetime.strptime(message['date'], '%Y-%m-%d %H:%M:%S.%f')
2020 summary.append(_CommentSummary(
2021 date=date,
2022 disapproval=bool(message['disapproval']),
2023 approval=bool(message['approval']),
2024 sender=message['sender'],
2025 message=message['text'],
2026 ))
2027 return summary
2028
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002029 def GetStatus(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002030 """Applies a rough heuristic to give a simple summary of an issue's review
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002031 or CQ status, assuming adherence to a common workflow.
2032
2033 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gablea1bab272017-04-11 16:38:18 -07002034 * 'error' - error from review tool (including deleted issues)
2035 * 'unsent' - not sent for review
2036 * 'waiting' - waiting for review
2037 * 'reply' - waiting for owner to reply to review
2038 * 'not lgtm' - Code-Review label has been set negatively
2039 * 'lgtm' - LGTM from at least one approved reviewer
2040 * 'commit' - in the commit queue
2041 * 'closed' - closed
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002042 """
2043 if not self.GetIssue():
2044 return None
2045
2046 try:
2047 props = self.GetIssueProperties()
2048 except urllib2.HTTPError:
2049 return 'error'
2050
2051 if props.get('closed'):
2052 # Issue is closed.
2053 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00002054 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002055 # Issue is in the commit queue.
2056 return 'commit'
2057
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002058 messages = props.get('messages') or []
Aaron Gablea1bab272017-04-11 16:38:18 -07002059 if not messages:
2060 # No message was sent.
2061 return 'unsent'
2062
2063 if get_approving_reviewers(props):
2064 return 'lgtm'
2065 elif get_approving_reviewers(props, disapproval=True):
2066 return 'not lgtm'
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002067
tandrii9d2c7a32016-06-22 03:42:45 -07002068 # Skip CQ messages that don't require owner's action.
2069 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
2070 if 'Dry run:' in messages[-1]['text']:
2071 messages.pop()
2072 elif 'The CQ bit was unchecked' in messages[-1]['text']:
2073 # This message always follows prior messages from CQ,
2074 # so skip this too.
2075 messages.pop()
2076 else:
2077 # This is probably a CQ messages warranting user attention.
2078 break
2079
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002080 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07002081 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002082 return 'reply'
2083 return 'waiting'
2084
dsansomee2d6fd92016-09-08 00:10:47 -07002085 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01002086 self.RpcServer().update_description(self.GetIssue(), description)
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002087
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002088 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002089 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002090
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002091 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002092 return self.SetFlags({flag: value})
2093
2094 def SetFlags(self, flags):
2095 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002096 """
phajdan.jr68598232016-08-10 03:28:28 -07002097 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002098 try:
tandrii4b233bd2016-07-06 03:50:29 -07002099 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002100 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002101 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002102 if e.code == 404:
2103 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2104 if e.code == 403:
2105 DieWithError(
2106 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002107 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002108 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002109
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002110 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002111 """Returns an upload.RpcServer() to access this review's rietveld instance.
2112 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002113 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002114 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002115 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01002116 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002117 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002118
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002119 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002120 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002121 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002122
tandrii5d48c322016-08-18 16:19:37 -07002123 @classmethod
2124 def PatchsetConfigKey(cls):
2125 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002126
tandrii5d48c322016-08-18 16:19:37 -07002127 @classmethod
2128 def CodereviewServerConfigKey(cls):
2129 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002130
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002131 def GetRietveldObjForPresubmit(self):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002132 return self.RpcServer()
2133
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002134 def SetCQState(self, new_state):
2135 props = self.GetIssueProperties()
2136 if props.get('private'):
2137 DieWithError('Cannot set-commit on private issue')
2138
2139 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002140 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002141 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002142 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002143 else:
tandrii4b233bd2016-07-06 03:50:29 -07002144 assert new_state == _CQState.DRY_RUN
2145 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002146
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002147 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002148 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002149 # PatchIssue should never be called with a dirty tree. It is up to the
2150 # caller to check this, but just in case we assert here since the
2151 # consequences of the caller not checking this could be dire.
2152 assert(not git_common.is_dirty_git_tree('apply'))
2153 assert(parsed_issue_arg.valid)
2154 self._changelist.issue = parsed_issue_arg.issue
2155 if parsed_issue_arg.hostname:
2156 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2157
skobes6468b902016-10-24 08:45:10 -07002158 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2159 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2160 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002161 try:
skobes6468b902016-10-24 08:45:10 -07002162 scm_obj.apply_patch(patchset_object)
2163 except Exception as e:
2164 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002165 return 1
2166
2167 # If we had an issue, commit the current state and register the issue.
2168 if not nocommit:
Aaron Gabled343c632017-03-15 11:02:26 -07002169 self.SetIssue(self.GetIssue())
2170 self.SetPatchset(patchset)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002171 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2172 'patch from issue %(i)s at patchset '
2173 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2174 % {'i': self.GetIssue(), 'p': patchset})])
vapiera7fbd5a2016-06-16 09:17:49 -07002175 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002176 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002177 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002178 return 0
2179
2180 @staticmethod
2181 def ParseIssueURL(parsed_url):
2182 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2183 return None
wychen3c1c1722016-08-04 11:46:36 -07002184 # Rietveld patch: https://domain/<number>/#ps<patchset>
2185 match = re.match(r'/(\d+)/$', parsed_url.path)
2186 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2187 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002188 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002189 issue=int(match.group(1)),
2190 patchset=int(match2.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002191 hostname=parsed_url.netloc,
2192 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002193 # Typical url: https://domain/<issue_number>[/[other]]
2194 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2195 if match:
skobes6468b902016-10-24 08:45:10 -07002196 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002197 issue=int(match.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002198 hostname=parsed_url.netloc,
2199 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002200 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2201 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2202 if match:
skobes6468b902016-10-24 08:45:10 -07002203 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002204 issue=int(match.group(1)),
2205 patchset=int(match.group(2)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002206 hostname=parsed_url.netloc,
2207 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002208 return None
2209
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002210 def CMDUploadChange(self, options, args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002211 """Upload the patch to Rietveld."""
2212 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2213 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002214 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2215 if options.emulate_svn_auto_props:
2216 upload_args.append('--emulate_svn_auto_props')
2217
2218 change_desc = None
2219
2220 if options.email is not None:
2221 upload_args.extend(['--email', options.email])
2222
2223 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002224 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002225 upload_args.extend(['--title', options.title])
2226 if options.message:
2227 upload_args.extend(['--message', options.message])
2228 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002229 print('This branch is associated with issue %s. '
2230 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002231 else:
nodirca166002016-06-27 10:59:51 -07002232 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002233 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002234 if options.message:
2235 message = options.message
2236 else:
2237 message = CreateDescriptionFromLog(args)
2238 if options.title:
2239 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002240 change_desc = ChangeDescription(message)
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002241 if options.reviewers or options.add_owners_to:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002242 change_desc.update_reviewers(options.reviewers, options.tbrs,
2243 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002244 if not options.force:
Aaron Gable3a16ed12017-03-23 10:51:55 -07002245 change_desc.prompt(bug=options.bug, git_footer=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002246
2247 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002248 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002249 return 1
2250
2251 upload_args.extend(['--message', change_desc.description])
2252 if change_desc.get_reviewers():
2253 upload_args.append('--reviewers=%s' % ','.join(
2254 change_desc.get_reviewers()))
2255 if options.send_mail:
2256 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002257 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002258 upload_args.append('--send_mail')
2259
2260 # We check this before applying rietveld.private assuming that in
2261 # rietveld.cc only addresses which we can send private CLs to are listed
2262 # if rietveld.private is set, and so we should ignore rietveld.cc only
2263 # when --private is specified explicitly on the command line.
2264 if options.private:
2265 logging.warn('rietveld.cc is ignored since private flag is specified. '
2266 'You need to review and add them manually if necessary.')
2267 cc = self.GetCCListWithoutDefault()
2268 else:
2269 cc = self.GetCCList()
2270 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002271 if change_desc.get_cced():
2272 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002273 if cc:
2274 upload_args.extend(['--cc', cc])
2275
2276 if options.private or settings.GetDefaultPrivateFlag() == "True":
2277 upload_args.append('--private')
2278
2279 upload_args.extend(['--git_similarity', str(options.similarity)])
2280 if not options.find_copies:
2281 upload_args.extend(['--git_no_find_copies'])
2282
2283 # Include the upstream repo's URL in the change -- this is useful for
2284 # projects that have their source spread across multiple repos.
2285 remote_url = self.GetGitBaseUrlFromConfig()
2286 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002287 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2288 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2289 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002290 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002291 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002292 target_ref = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002293 if target_ref:
2294 upload_args.extend(['--target_ref', target_ref])
2295
2296 # Look for dependent patchsets. See crbug.com/480453 for more details.
2297 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2298 upstream_branch = ShortBranchName(upstream_branch)
2299 if remote is '.':
2300 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002301 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002302 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002303 print()
2304 print('Skipping dependency patchset upload because git config '
2305 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2306 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002307 else:
2308 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002309 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002310 auth_config=auth_config)
2311 branch_cl_issue_url = branch_cl.GetIssueURL()
2312 branch_cl_issue = branch_cl.GetIssue()
2313 branch_cl_patchset = branch_cl.GetPatchset()
2314 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2315 upload_args.extend(
2316 ['--depends_on_patchset', '%s:%s' % (
2317 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002318 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002319 '\n'
2320 'The current branch (%s) is tracking a local branch (%s) with '
2321 'an associated CL.\n'
2322 'Adding %s/#ps%s as a dependency patchset.\n'
2323 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2324 branch_cl_patchset))
2325
2326 project = settings.GetProject()
2327 if project:
2328 upload_args.extend(['--project', project])
Aaron Gable665a4392017-06-29 10:53:46 -07002329 else:
2330 print()
2331 print('WARNING: Uploading without a project specified. Please ensure '
2332 'your repo\'s codereview.settings has a "PROJECT: foo" line.')
2333 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002334
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002335 try:
2336 upload_args = ['upload'] + upload_args + args
2337 logging.info('upload.RealMain(%s)', upload_args)
2338 issue, patchset = upload.RealMain(upload_args)
2339 issue = int(issue)
2340 patchset = int(patchset)
2341 except KeyboardInterrupt:
2342 sys.exit(1)
2343 except:
2344 # If we got an exception after the user typed a description for their
2345 # change, back up the description before re-raising.
2346 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002347 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002348 raise
2349
2350 if not self.GetIssue():
2351 self.SetIssue(issue)
2352 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002353 return 0
2354
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002355
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002356class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002357 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002358 # auth_config is Rietveld thing, kept here to preserve interface only.
2359 super(_GerritChangelistImpl, self).__init__(changelist)
2360 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002361 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002362 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002363 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002364 # Map from change number (issue) to its detail cache.
2365 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002366
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002367 if codereview_host is not None:
2368 assert not codereview_host.startswith('https://'), codereview_host
2369 self._gerrit_host = codereview_host
2370 self._gerrit_server = 'https://%s' % codereview_host
2371
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002372 def _GetGerritHost(self):
2373 # Lazy load of configs.
2374 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002375 if self._gerrit_host and '.' not in self._gerrit_host:
2376 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2377 # This happens for internal stuff http://crbug.com/614312.
2378 parsed = urlparse.urlparse(self.GetRemoteUrl())
2379 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002380 print('WARNING: using non-https URLs for remote is likely broken\n'
tandriie32e3ea2016-06-22 02:52:48 -07002381 ' Your current remote is: %s' % self.GetRemoteUrl())
2382 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2383 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002384 return self._gerrit_host
2385
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002386 def _GetGitHost(self):
2387 """Returns git host to be used when uploading change to Gerrit."""
2388 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2389
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002390 def GetCodereviewServer(self):
2391 if not self._gerrit_server:
2392 # If we're on a branch then get the server potentially associated
2393 # with that branch.
2394 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002395 self._gerrit_server = self._GitGetBranchConfigValue(
2396 self.CodereviewServerConfigKey())
2397 if self._gerrit_server:
2398 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002399 if not self._gerrit_server:
2400 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2401 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002402 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002403 parts[0] = parts[0] + '-review'
2404 self._gerrit_host = '.'.join(parts)
2405 self._gerrit_server = 'https://%s' % self._gerrit_host
2406 return self._gerrit_server
2407
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002408 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002409 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002410 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002411
tandrii5d48c322016-08-18 16:19:37 -07002412 @classmethod
2413 def PatchsetConfigKey(cls):
2414 return 'gerritpatchset'
2415
2416 @classmethod
2417 def CodereviewServerConfigKey(cls):
2418 return 'gerritserver'
2419
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002420 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002421 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002422 if settings.GetGerritSkipEnsureAuthenticated():
2423 # For projects with unusual authentication schemes.
2424 # See http://crbug.com/603378.
2425 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002426 # Lazy-loader to identify Gerrit and Git hosts.
2427 if gerrit_util.GceAuthenticator.is_gce():
2428 return
2429 self.GetCodereviewServer()
2430 git_host = self._GetGitHost()
2431 assert self._gerrit_server and self._gerrit_host
2432 cookie_auth = gerrit_util.CookiesAuthenticator()
2433
2434 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2435 git_auth = cookie_auth.get_auth_header(git_host)
2436 if gerrit_auth and git_auth:
2437 if gerrit_auth == git_auth:
2438 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002439 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002440 print((
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002441 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002442 ' %s\n'
2443 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002444 ' Consider running the following command:\n'
2445 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002446 ' %s\n'
Andrii Shyshkalov8e4576f2017-05-10 15:46:53 +02002447 ' %s') %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002448 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002449 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002450 cookie_auth.get_new_password_message(git_host)))
2451 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002452 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002453 return
2454 else:
2455 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02002456 ([] if gerrit_auth else [self._gerrit_host]) +
2457 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002458 DieWithError('Credentials for the following hosts are required:\n'
2459 ' %s\n'
2460 'These are read from %s (or legacy %s)\n'
2461 '%s' % (
2462 '\n '.join(missing),
2463 cookie_auth.get_gitcookies_path(),
2464 cookie_auth.get_netrc_path(),
2465 cookie_auth.get_new_password_message(git_host)))
2466
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002467 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002468 if not self.GetIssue():
2469 return
2470
2471 # Warm change details cache now to avoid RPCs later, reducing latency for
2472 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002473 self._GetChangeDetail(
2474 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002475
2476 status = self._GetChangeDetail()['status']
2477 if status in ('MERGED', 'ABANDONED'):
2478 DieWithError('Change %s has been %s, new uploads are not allowed' %
2479 (self.GetIssueURL(),
2480 'submitted' if status == 'MERGED' else 'abandoned'))
2481
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002482 if gerrit_util.GceAuthenticator.is_gce():
2483 return
2484 cookies_user = gerrit_util.CookiesAuthenticator().get_auth_email(
2485 self._GetGerritHost())
2486 if self.GetIssueOwner() == cookies_user:
2487 return
2488 logging.debug('change %s owner is %s, cookies user is %s',
2489 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002490 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002491 # so ask what Gerrit thinks of this user.
2492 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2493 if details['email'] == self.GetIssueOwner():
2494 return
2495 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002496 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002497 'as %s.\n'
2498 'Uploading may fail due to lack of permissions.' %
2499 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2500 confirm_or_exit(action='upload')
2501
2502
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002503 def _PostUnsetIssueProperties(self):
2504 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002505 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002506
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002507 def GetRietveldObjForPresubmit(self):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002508 class ThisIsNotRietveldIssue(object):
2509 def __nonzero__(self):
2510 # This is a hack to make presubmit_support think that rietveld is not
2511 # defined, yet still ensure that calls directly result in a decent
2512 # exception message below.
2513 return False
2514
2515 def __getattr__(self, attr):
2516 print(
2517 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2518 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002519 'Please, either change your PRESUBMIT to not use rietveld_obj.%s,\n'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002520 'or use Rietveld for codereview.\n'
2521 'See also http://crbug.com/579160.' % attr)
2522 raise NotImplementedError()
2523 return ThisIsNotRietveldIssue()
2524
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002525 def GetGerritObjForPresubmit(self):
2526 return presubmit_support.GerritAccessor(self._GetGerritHost())
2527
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002528 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002529 """Apply a rough heuristic to give a simple summary of an issue's review
2530 or CQ status, assuming adherence to a common workflow.
2531
2532 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002533 * 'error' - error from review tool (including deleted issues)
2534 * 'unsent' - no reviewers added
2535 * 'waiting' - waiting for review
2536 * 'reply' - waiting for uploader to reply to review
2537 * 'lgtm' - Code-Review label has been set
2538 * 'commit' - in the commit queue
2539 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002540 """
2541 if not self.GetIssue():
2542 return None
2543
2544 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002545 data = self._GetChangeDetail([
2546 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002547 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002548 return 'error'
2549
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002550 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002551 return 'closed'
2552
Aaron Gable9ab38c62017-04-06 14:36:33 -07002553 if data['labels'].get('Commit-Queue', {}).get('approved'):
2554 # The section will have an "approved" subsection if anyone has voted
2555 # the maximum value on the label.
2556 return 'commit'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002557
Aaron Gable9ab38c62017-04-06 14:36:33 -07002558 if data['labels'].get('Code-Review', {}).get('approved'):
2559 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002560
2561 if not data.get('reviewers', {}).get('REVIEWER', []):
2562 return 'unsent'
2563
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002564 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002565 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2566 last_message_author = messages.pop().get('author', {})
2567 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002568 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2569 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002570 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002571 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002572 if last_message_author.get('_account_id') == owner:
2573 # Most recent message was by owner.
2574 return 'waiting'
2575 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002576 # Some reply from non-owner.
2577 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002578
2579 # Somehow there are no messages even though there are reviewers.
2580 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002581
2582 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002583 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002584 patchset = data['revisions'][data['current_revision']]['_number']
2585 self.SetPatchset(patchset)
2586 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002587
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002588 def FetchDescription(self, force=False):
2589 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2590 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002591 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002592 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002593
dsansomee2d6fd92016-09-08 00:10:47 -07002594 def UpdateDescriptionRemote(self, description, force=False):
2595 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2596 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002597 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002598 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002599 'unpublished edit. Either publish the edit in the Gerrit web UI '
2600 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002601
2602 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2603 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002604 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002605 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002606
Aaron Gable636b13f2017-07-14 10:42:48 -07002607 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002608 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
Aaron Gable636b13f2017-07-14 10:42:48 -07002609 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002610
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002611 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002612 # DETAILED_ACCOUNTS is to get emails in accounts.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002613 messages = self._GetChangeDetail(
2614 options=['MESSAGES', 'DETAILED_ACCOUNTS']).get('messages', [])
2615 file_comments = gerrit_util.GetChangeComments(
2616 self._GetGerritHost(), self.GetIssue())
2617
2618 # Build dictionary of file comments for easy access and sorting later.
2619 # {author+date: {path: {patchset: {line: url+message}}}}
2620 comments = collections.defaultdict(
2621 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2622 for path, line_comments in file_comments.iteritems():
2623 for comment in line_comments:
2624 if comment.get('tag', '').startswith('autogenerated'):
2625 continue
2626 key = (comment['author']['email'], comment['updated'])
2627 if comment.get('side', 'REVISION') == 'PARENT':
2628 patchset = 'Base'
2629 else:
2630 patchset = 'PS%d' % comment['patch_set']
2631 line = comment.get('line', 0)
2632 url = ('https://%s/c/%s/%s/%s#%s%s' %
2633 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2634 'b' if comment.get('side') == 'PARENT' else '',
2635 str(line) if line else ''))
2636 comments[key][path][patchset][line] = (url, comment['message'])
2637
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002638 summary = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002639 for msg in messages:
2640 # Don't bother showing autogenerated messages.
2641 if msg.get('tag') and msg.get('tag').startswith('autogenerated'):
2642 continue
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002643 # Gerrit spits out nanoseconds.
2644 assert len(msg['date'].split('.')[-1]) == 9
2645 date = datetime.datetime.strptime(msg['date'][:-3],
2646 '%Y-%m-%d %H:%M:%S.%f')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002647 message = msg['message']
2648 key = (msg['author']['email'], msg['date'])
2649 if key in comments:
2650 message += '\n'
2651 for path, patchsets in sorted(comments.get(key, {}).items()):
2652 if readable:
2653 message += '\n%s' % path
2654 for patchset, lines in sorted(patchsets.items()):
2655 for line, (url, content) in sorted(lines.items()):
2656 if line:
2657 line_str = 'Line %d' % line
2658 path_str = '%s:%d:' % (path, line)
2659 else:
2660 line_str = 'File comment'
2661 path_str = '%s:0:' % path
2662 if readable:
2663 message += '\n %s, %s: %s' % (patchset, line_str, url)
2664 message += '\n %s\n' % content
2665 else:
2666 message += '\n%s ' % path_str
2667 message += '\n%s\n' % content
2668
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002669 summary.append(_CommentSummary(
2670 date=date,
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002671 message=message,
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002672 sender=msg['author']['email'],
2673 # These could be inferred from the text messages and correlated with
2674 # Code-Review label maximum, however this is not reliable.
2675 # Leaving as is until the need arises.
2676 approval=False,
2677 disapproval=False,
2678 ))
2679 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002680
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002681 def CloseIssue(self):
2682 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2683
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002684 def SubmitIssue(self, wait_for_merge=True):
2685 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2686 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002687
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002688 def _GetChangeDetail(self, options=None, issue=None,
2689 no_cache=False):
2690 """Returns details of the issue by querying Gerrit and caching results.
2691
2692 If fresh data is needed, set no_cache=True which will clear cache and
2693 thus new data will be fetched from Gerrit.
2694 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002695 options = options or []
2696 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002697 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002698
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002699 # Optimization to avoid multiple RPCs:
2700 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2701 'CURRENT_COMMIT' not in options):
2702 options.append('CURRENT_COMMIT')
2703
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002704 # Normalize issue and options for consistent keys in cache.
2705 issue = str(issue)
2706 options = [o.upper() for o in options]
2707
2708 # Check in cache first unless no_cache is True.
2709 if no_cache:
2710 self._detail_cache.pop(issue, None)
2711 else:
2712 options_set = frozenset(options)
2713 for cached_options_set, data in self._detail_cache.get(issue, []):
2714 # Assumption: data fetched before with extra options is suitable
2715 # for return for a smaller set of options.
2716 # For example, if we cached data for
2717 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2718 # and request is for options=[CURRENT_REVISION],
2719 # THEN we can return prior cached data.
2720 if options_set.issubset(cached_options_set):
2721 return data
2722
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002723 try:
Aaron Gable19ee16c2017-04-18 11:56:35 -07002724 data = gerrit_util.GetChangeDetail(
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002725 self._GetGerritHost(), str(issue), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002726 except gerrit_util.GerritError as e:
2727 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002728 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002729 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002730
2731 self._detail_cache.setdefault(issue, []).append((frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002732 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002733
agable32978d92016-11-01 12:55:02 -07002734 def _GetChangeCommit(self, issue=None):
2735 issue = issue or self.GetIssue()
2736 assert issue, 'issue is required to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002737 try:
2738 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2739 except gerrit_util.GerritError as e:
2740 if e.http_status == 404:
2741 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
2742 raise
agable32978d92016-11-01 12:55:02 -07002743 return data
2744
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002745 def CMDLand(self, force, bypass_hooks, verbose):
2746 if git_common.is_dirty_git_tree('land'):
2747 return 1
tandriid60367b2016-06-22 05:25:12 -07002748 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2749 if u'Commit-Queue' in detail.get('labels', {}):
2750 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002751 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2752 'which can test and land changes for you. '
2753 'Are you sure you wish to bypass it?\n',
2754 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002755
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002756 differs = True
tandriic4344b52016-08-29 06:04:54 -07002757 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002758 # Note: git diff outputs nothing if there is no diff.
2759 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002760 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002761 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002762 if detail['current_revision'] == last_upload:
2763 differs = False
2764 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002765 print('WARNING: Local branch contents differ from latest uploaded '
2766 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002767 if differs:
2768 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002769 confirm_or_exit(
2770 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2771 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002772 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002773 elif not bypass_hooks:
2774 hook_results = self.RunHook(
2775 committing=True,
2776 may_prompt=not force,
2777 verbose=verbose,
2778 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2779 if not hook_results.should_continue():
2780 return 1
2781
2782 self.SubmitIssue(wait_for_merge=True)
2783 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002784 links = self._GetChangeCommit().get('web_links', [])
2785 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002786 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002787 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002788 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002789 return 0
2790
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002791 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002792 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002793 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002794 assert not directory
2795 assert parsed_issue_arg.valid
2796
2797 self._changelist.issue = parsed_issue_arg.issue
2798
2799 if parsed_issue_arg.hostname:
2800 self._gerrit_host = parsed_issue_arg.hostname
2801 self._gerrit_server = 'https://%s' % self._gerrit_host
2802
tandriic2405f52016-10-10 08:13:15 -07002803 try:
2804 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002805 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002806 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002807
2808 if not parsed_issue_arg.patchset:
2809 # Use current revision by default.
2810 revision_info = detail['revisions'][detail['current_revision']]
2811 patchset = int(revision_info['_number'])
2812 else:
2813 patchset = parsed_issue_arg.patchset
2814 for revision_info in detail['revisions'].itervalues():
2815 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2816 break
2817 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002818 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002819 (parsed_issue_arg.patchset, self.GetIssue()))
2820
Aaron Gable697a91b2018-01-19 15:20:15 -08002821 remote_url = self._changelist.GetRemoteUrl()
2822 if remote_url.endswith('.git'):
2823 remote_url = remote_url[:-len('.git')]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002824 fetch_info = revision_info['fetch']['http']
Aaron Gable697a91b2018-01-19 15:20:15 -08002825
2826 if remote_url != fetch_info['url']:
2827 DieWithError('Trying to patch a change from %s but this repo appears '
2828 'to be %s.' % (fetch_info['url'], remote_url))
2829
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002830 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002831
Aaron Gable62619a32017-06-16 08:22:09 -07002832 if force:
2833 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2834 print('Checked out commit for change %i patchset %i locally' %
2835 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002836 elif nocommit:
2837 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2838 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002839 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002840 RunGit(['cherry-pick', 'FETCH_HEAD'])
2841 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002842 (parsed_issue_arg.issue, patchset))
2843 print('Note: this created a local commit which does not have '
2844 'the same hash as the one uploaded for review. This will make '
2845 'uploading changes based on top of this branch difficult.\n'
2846 'If you want to do that, use "git cl patch --force" instead.')
2847
Stefan Zagerd08043c2017-10-12 12:07:02 -07002848 if self.GetBranch():
2849 self.SetIssue(parsed_issue_arg.issue)
2850 self.SetPatchset(patchset)
2851 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2852 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2853 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2854 else:
2855 print('WARNING: You are in detached HEAD state.\n'
2856 'The patch has been applied to your checkout, but you will not be '
2857 'able to upload a new patch set to the gerrit issue.\n'
2858 'Try using the \'-b\' option if you would like to work on a '
2859 'branch and/or upload a new patch set.')
2860
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002861 return 0
2862
2863 @staticmethod
2864 def ParseIssueURL(parsed_url):
2865 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2866 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002867 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2868 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002869 # Short urls like https://domain/<issue_number> can be used, but don't allow
2870 # specifying the patchset (you'd 404), but we allow that here.
2871 if parsed_url.path == '/':
2872 part = parsed_url.fragment
2873 else:
2874 part = parsed_url.path
Aaron Gable01b91062017-08-24 17:48:40 -07002875 match = re.match('(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002876 if match:
2877 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002878 issue=int(match.group(3)),
2879 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002880 hostname=parsed_url.netloc,
2881 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002882 return None
2883
tandrii16e0b4e2016-06-07 10:34:28 -07002884 def _GerritCommitMsgHookCheck(self, offer_removal):
2885 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2886 if not os.path.exists(hook):
2887 return
2888 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2889 # custom developer made one.
2890 data = gclient_utils.FileRead(hook)
2891 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2892 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002893 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002894 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002895 'and may interfere with it in subtle ways.\n'
2896 'We recommend you remove the commit-msg hook.')
2897 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002898 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002899 gclient_utils.rm_file_or_tree(hook)
2900 print('Gerrit commit-msg hook removed.')
2901 else:
2902 print('OK, will keep Gerrit commit-msg hook in place.')
2903
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002904 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002905 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002906 if options.squash and options.no_squash:
2907 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002908
2909 if not options.squash and not options.no_squash:
2910 # Load default for user, repo, squash=true, in this order.
2911 options.squash = settings.GetSquashGerritUploads()
2912 elif options.no_squash:
2913 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002914
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002915 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002916 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002917
Aaron Gableb56ad332017-01-06 15:24:31 -08002918 # This may be None; default fallback value is determined in logic below.
2919 title = options.title
2920
Dominic Battre7d1c4842017-10-27 09:17:28 +02002921 # Extract bug number from branch name.
2922 bug = options.bug
2923 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2924 if not bug and match:
2925 bug = match.group(1)
2926
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002927 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002928 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002929 if self.GetIssue():
2930 # Try to get the message from a previous upload.
2931 message = self.GetDescription()
2932 if not message:
2933 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002934 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002935 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002936 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002937 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002938 # When uploading a subsequent patchset, -m|--message is taken
2939 # as the patchset title if --title was not provided.
2940 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002941 else:
2942 default_title = RunGit(
2943 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002944 if options.force:
2945 title = default_title
2946 else:
2947 title = ask_for_data(
2948 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002949 change_id = self._GetChangeDetail()['change_id']
2950 while True:
2951 footer_change_ids = git_footers.get_footer_change_id(message)
2952 if footer_change_ids == [change_id]:
2953 break
2954 if not footer_change_ids:
2955 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002956 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002957 continue
2958 # There is already a valid footer but with different or several ids.
2959 # Doing this automatically is non-trivial as we don't want to lose
2960 # existing other footers, yet we want to append just 1 desired
2961 # Change-Id. Thus, just create a new footer, but let user verify the
2962 # new description.
2963 message = '%s\n\nChange-Id: %s' % (message, change_id)
2964 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002965 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002966 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002967 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002968 'Please, check the proposed correction to the description, '
2969 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2970 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2971 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002972 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002973 if not options.force:
2974 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002975 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002976 message = change_desc.description
2977 if not message:
2978 DieWithError("Description is empty. Aborting...")
2979 # Continue the while loop.
2980 # Sanity check of this code - we should end up with proper message
2981 # footer.
2982 assert [change_id] == git_footers.get_footer_change_id(message)
2983 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002984 else: # if not self.GetIssue()
2985 if options.message:
2986 message = options.message
2987 else:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002988 message = CreateDescriptionFromLog(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002989 if options.title:
2990 message = options.title + '\n\n' + message
2991 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002992
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002993 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02002994 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002995 # On first upload, patchset title is always this string, while
2996 # --title flag gets converted to first line of message.
2997 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002998 if not change_desc.description:
2999 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02003000 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003001 if len(change_ids) > 1:
3002 DieWithError('too many Change-Id footers, at most 1 allowed.')
3003 if not change_ids:
3004 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02003005 change_desc.set_description(git_footers.add_footer_change_id(
3006 change_desc.description,
3007 GenerateGerritChangeId(change_desc.description)))
3008 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003009 assert len(change_ids) == 1
3010 change_id = change_ids[0]
3011
Robert Iannuccidb02dd02017-04-19 12:18:20 -07003012 if options.reviewers or options.tbrs or options.add_owners_to:
3013 change_desc.update_reviewers(options.reviewers, options.tbrs,
3014 options.add_owners_to, change)
3015
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003016 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003017 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
3018 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003019 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07003020 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
3021 desc_tempfile.write(change_desc.description)
3022 desc_tempfile.close()
3023 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
3024 '-F', desc_tempfile.name]).strip()
3025 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003026 else:
3027 change_desc = ChangeDescription(
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003028 options.message or CreateDescriptionFromLog(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003029 if not change_desc.description:
3030 DieWithError("Description is empty. Aborting...")
3031
3032 if not git_footers.get_footer_change_id(change_desc.description):
3033 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003034 change_desc.set_description(
3035 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07003036 if options.reviewers or options.tbrs or options.add_owners_to:
3037 change_desc.update_reviewers(options.reviewers, options.tbrs,
3038 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003039 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003040 # For no-squash mode, we assume the remote called "origin" is the one we
3041 # want. It is not worthwhile to support different workflows for
3042 # no-squash mode.
3043 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003044 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
3045
3046 assert change_desc
3047 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
3048 ref_to_push)]).splitlines()
3049 if len(commits) > 1:
3050 print('WARNING: This will upload %d commits. Run the following command '
3051 'to see which commits will be uploaded: ' % len(commits))
3052 print('git log %s..%s' % (parent, ref_to_push))
3053 print('You can also use `git squash-branch` to squash these into a '
3054 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003055 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003056
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003057 if options.reviewers or options.tbrs or options.add_owners_to:
3058 change_desc.update_reviewers(options.reviewers, options.tbrs,
3059 options.add_owners_to, change)
3060
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00003061 # Extra options that can be specified at push time. Doc:
3062 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003063 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003064
Aaron Gable844cf292017-06-28 11:32:59 -07003065 # By default, new changes are started in WIP mode, and subsequent patchsets
3066 # don't send email. At any time, passing --send-mail will mark the change
3067 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07003068 if options.send_mail:
3069 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07003070 refspec_opts.append('notify=ALL')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003071 elif not self.GetIssue():
3072 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07003073 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003074 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07003075
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003076 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07003077 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003078
Aaron Gable9b713dd2016-12-14 16:04:21 -08003079 if title:
Nick Carter8692b182017-11-06 16:30:38 -08003080 # Punctuation and whitespace in |title| must be percent-encoded.
3081 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00003082
agablec6787972016-09-09 16:13:34 -07003083 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07003084 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07003085
rmistry9eadede2016-09-19 11:22:43 -07003086 if options.topic:
3087 # Documentation on Gerrit topics is here:
3088 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003089 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07003090
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003091 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08003092 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003093 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08003094 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003095 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
3096
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003097 refspec_suffix = ''
3098 if refspec_opts:
3099 refspec_suffix = '%' + ','.join(refspec_opts)
3100 assert ' ' not in refspec_suffix, (
3101 'spaces not allowed in refspec: "%s"' % refspec_suffix)
3102 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
3103
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003104 try:
3105 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003106 ['git', 'push', self.GetRemoteUrl(), refspec],
3107 print_stdout=True,
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003108 # Flush after every line: useful for seeing progress when running as
3109 # recipe.
3110 filter_fn=lambda _: sys.stdout.flush())
3111 except subprocess2.CalledProcessError:
3112 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003113 'for the reason of the failure.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003114 'Hint: run command below to diagnose common Git/Gerrit '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003115 'credential problems:\n'
3116 ' git cl creds-check\n',
3117 change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003118
3119 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07003120 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003121 change_numbers = [m.group(1)
3122 for m in map(regex.match, push_stdout.splitlines())
3123 if m]
3124 if len(change_numbers) != 1:
3125 DieWithError(
3126 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11003127 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003128 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07003129 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07003130
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003131 reviewers = sorted(change_desc.get_reviewers())
3132
tandrii88189772016-09-29 04:29:57 -07003133 # Add cc's from the CC_LIST and --cc flag (if any).
Aaron Gabled1052492017-05-15 15:05:34 -07003134 if not options.private:
3135 cc = self.GetCCList().split(',')
3136 else:
3137 cc = []
tandrii88189772016-09-29 04:29:57 -07003138 if options.cc:
3139 cc.extend(options.cc)
3140 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07003141 if change_desc.get_cced():
3142 cc.extend(change_desc.get_cced())
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003143
3144 gerrit_util.AddReviewers(
3145 self._GetGerritHost(), self.GetIssue(), reviewers, cc,
3146 notify=bool(options.send_mail))
3147
Aaron Gablefd238082017-06-07 13:42:34 -07003148 if change_desc.get_reviewers(tbr_only=True):
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003149 labels = self._GetChangeDetail(['LABELS']).get('labels', {})
3150 score = 1
3151 if 'Code-Review' in labels and 'values' in labels['Code-Review']:
3152 score = max([int(x) for x in labels['Code-Review']['values'].keys()])
3153 print('Adding self-LGTM (Code-Review +%d) because of TBRs.' % score)
Aaron Gablefd238082017-06-07 13:42:34 -07003154 gerrit_util.SetReview(
3155 self._GetGerritHost(), self.GetIssue(),
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003156 msg='Self-approving for TBR',
3157 labels={'Code-Review': score})
Aaron Gablefd238082017-06-07 13:42:34 -07003158
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003159 return 0
3160
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003161 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
3162 change_desc):
3163 """Computes parent of the generated commit to be uploaded to Gerrit.
3164
3165 Returns revision or a ref name.
3166 """
3167 if custom_cl_base:
3168 # Try to avoid creating additional unintended CLs when uploading, unless
3169 # user wants to take this risk.
3170 local_ref_of_target_remote = self.GetRemoteBranch()[1]
3171 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
3172 local_ref_of_target_remote])
3173 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003174 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003175 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
3176 'If you proceed with upload, more than 1 CL may be created by '
3177 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
3178 'If you are certain that specified base `%s` has already been '
3179 'uploaded to Gerrit as another CL, you may proceed.\n' %
3180 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
3181 if not force:
3182 confirm_or_exit(
3183 'Do you take responsibility for cleaning up potential mess '
3184 'resulting from proceeding with upload?',
3185 action='upload')
3186 return custom_cl_base
3187
Aaron Gablef97e33d2017-03-30 15:44:27 -07003188 if remote != '.':
3189 return self.GetCommonAncestorWithUpstream()
3190
3191 # If our upstream branch is local, we base our squashed commit on its
3192 # squashed version.
3193 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
3194
Aaron Gablef97e33d2017-03-30 15:44:27 -07003195 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07003196 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003197
3198 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003199 # TODO(tandrii): consider checking parent change in Gerrit and using its
3200 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
3201 # the tree hash of the parent branch. The upside is less likely bogus
3202 # requests to reupload parent change just because it's uploadhash is
3203 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07003204 parent = RunGit(['config',
3205 'branch.%s.gerritsquashhash' % upstream_branch_name],
3206 error_ok=True).strip()
3207 # Verify that the upstream branch has been uploaded too, otherwise
3208 # Gerrit will create additional CLs when uploading.
3209 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
3210 RunGitSilent(['rev-parse', parent + ':'])):
3211 DieWithError(
3212 '\nUpload upstream branch %s first.\n'
3213 'It is likely that this branch has been rebased since its last '
3214 'upload, so you just need to upload it again.\n'
3215 '(If you uploaded it with --no-squash, then branch dependencies '
3216 'are not supported, and you should reupload with --squash.)'
3217 % upstream_branch_name,
3218 change_desc)
3219 return parent
3220
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003221 def _AddChangeIdToCommitMessage(self, options, args):
3222 """Re-commits using the current message, assumes the commit hook is in
3223 place.
3224 """
3225 log_desc = options.message or CreateDescriptionFromLog(args)
3226 git_command = ['commit', '--amend', '-m', log_desc]
3227 RunGit(git_command)
3228 new_log_desc = CreateDescriptionFromLog(args)
3229 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003230 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003231 return new_log_desc
3232 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00003233 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003234
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003235 def SetCQState(self, new_state):
3236 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003237 vote_map = {
3238 _CQState.NONE: 0,
3239 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003240 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003241 }
Aaron Gablefc62f762017-07-17 11:12:07 -07003242 labels = {'Commit-Queue': vote_map[new_state]}
3243 notify = False if new_state == _CQState.DRY_RUN else None
3244 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
3245 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003246
tandriie113dfd2016-10-11 10:20:12 -07003247 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003248 try:
3249 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003250 except GerritChangeNotExists:
3251 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003252
3253 if data['status'] in ('ABANDONED', 'MERGED'):
3254 return 'CL %s is closed' % self.GetIssue()
3255
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003256 def GetTryJobProperties(self, patchset=None):
3257 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07003258 data = self._GetChangeDetail(['ALL_REVISIONS'])
3259 patchset = int(patchset or self.GetPatchset())
3260 assert patchset
3261 revision_data = None # Pylint wants it to be defined.
3262 for revision_data in data['revisions'].itervalues():
3263 if int(revision_data['_number']) == patchset:
3264 break
3265 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003266 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003267 (patchset, self.GetIssue()))
3268 return {
3269 'patch_issue': self.GetIssue(),
3270 'patch_set': patchset or self.GetPatchset(),
3271 'patch_project': data['project'],
3272 'patch_storage': 'gerrit',
3273 'patch_ref': revision_data['fetch']['http']['ref'],
3274 'patch_repository_url': revision_data['fetch']['http']['url'],
3275 'patch_gerrit_url': self.GetCodereviewServer(),
3276 }
tandriie113dfd2016-10-11 10:20:12 -07003277
tandriide281ae2016-10-12 06:02:30 -07003278 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003279 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003280
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003281
3282_CODEREVIEW_IMPLEMENTATIONS = {
3283 'rietveld': _RietveldChangelistImpl,
3284 'gerrit': _GerritChangelistImpl,
3285}
3286
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003287
iannuccie53c9352016-08-17 14:40:40 -07003288def _add_codereview_issue_select_options(parser, extra=""):
3289 _add_codereview_select_options(parser)
3290
3291 text = ('Operate on this issue number instead of the current branch\'s '
3292 'implicit issue.')
3293 if extra:
3294 text += ' '+extra
3295 parser.add_option('-i', '--issue', type=int, help=text)
3296
3297
3298def _process_codereview_issue_select_options(parser, options):
3299 _process_codereview_select_options(parser, options)
3300 if options.issue is not None and not options.forced_codereview:
3301 parser.error('--issue must be specified with either --rietveld or --gerrit')
3302
3303
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003304def _add_codereview_select_options(parser):
3305 """Appends --gerrit and --rietveld options to force specific codereview."""
3306 parser.codereview_group = optparse.OptionGroup(
3307 parser, 'EXPERIMENTAL! Codereview override options')
3308 parser.add_option_group(parser.codereview_group)
3309 parser.codereview_group.add_option(
3310 '--gerrit', action='store_true',
3311 help='Force the use of Gerrit for codereview')
3312 parser.codereview_group.add_option(
3313 '--rietveld', action='store_true',
3314 help='Force the use of Rietveld for codereview')
3315
3316
3317def _process_codereview_select_options(parser, options):
3318 if options.gerrit and options.rietveld:
3319 parser.error('Options --gerrit and --rietveld are mutually exclusive')
3320 options.forced_codereview = None
3321 if options.gerrit:
3322 options.forced_codereview = 'gerrit'
3323 elif options.rietveld:
3324 options.forced_codereview = 'rietveld'
3325
3326
tandriif9aefb72016-07-01 09:06:51 -07003327def _get_bug_line_values(default_project, bugs):
3328 """Given default_project and comma separated list of bugs, yields bug line
3329 values.
3330
3331 Each bug can be either:
3332 * a number, which is combined with default_project
3333 * string, which is left as is.
3334
3335 This function may produce more than one line, because bugdroid expects one
3336 project per line.
3337
3338 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3339 ['v8:123', 'chromium:789']
3340 """
3341 default_bugs = []
3342 others = []
3343 for bug in bugs.split(','):
3344 bug = bug.strip()
3345 if bug:
3346 try:
3347 default_bugs.append(int(bug))
3348 except ValueError:
3349 others.append(bug)
3350
3351 if default_bugs:
3352 default_bugs = ','.join(map(str, default_bugs))
3353 if default_project:
3354 yield '%s:%s' % (default_project, default_bugs)
3355 else:
3356 yield default_bugs
3357 for other in sorted(others):
3358 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3359 yield other
3360
3361
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003362class ChangeDescription(object):
3363 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003364 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003365 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003366 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003367 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003368 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3369 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
3370 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
3371 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003372
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003373 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003374 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003375
agable@chromium.org42c20792013-09-12 17:34:49 +00003376 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003377 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003378 return '\n'.join(self._description_lines)
3379
3380 def set_description(self, desc):
3381 if isinstance(desc, basestring):
3382 lines = desc.splitlines()
3383 else:
3384 lines = [line.rstrip() for line in desc]
3385 while lines and not lines[0]:
3386 lines.pop(0)
3387 while lines and not lines[-1]:
3388 lines.pop(-1)
3389 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003390
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003391 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3392 """Rewrites the R=/TBR= line(s) as a single line each.
3393
3394 Args:
3395 reviewers (list(str)) - list of additional emails to use for reviewers.
3396 tbrs (list(str)) - list of additional emails to use for TBRs.
3397 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3398 the change that are missing OWNER coverage. If this is not None, you
3399 must also pass a value for `change`.
3400 change (Change) - The Change that should be used for OWNERS lookups.
3401 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003402 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003403 assert isinstance(tbrs, list), tbrs
3404
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003405 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003406 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003407
3408 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003409 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003410
3411 reviewers = set(reviewers)
3412 tbrs = set(tbrs)
3413 LOOKUP = {
3414 'TBR': tbrs,
3415 'R': reviewers,
3416 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003417
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003418 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003419 regexp = re.compile(self.R_LINE)
3420 matches = [regexp.match(line) for line in self._description_lines]
3421 new_desc = [l for i, l in enumerate(self._description_lines)
3422 if not matches[i]]
3423 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003424
agable@chromium.org42c20792013-09-12 17:34:49 +00003425 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003426
3427 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003428 for match in matches:
3429 if not match:
3430 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003431 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3432
3433 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003434 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003435 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003436 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003437 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003438 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003439 LOOKUP[add_owners_to].update(
3440 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003441
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003442 # If any folks ended up in both groups, remove them from tbrs.
3443 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003444
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003445 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3446 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003447
3448 # Put the new lines in the description where the old first R= line was.
3449 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3450 if 0 <= line_loc < len(self._description_lines):
3451 if new_tbr_line:
3452 self._description_lines.insert(line_loc, new_tbr_line)
3453 if new_r_line:
3454 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003455 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003456 if new_r_line:
3457 self.append_footer(new_r_line)
3458 if new_tbr_line:
3459 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003460
Aaron Gable3a16ed12017-03-23 10:51:55 -07003461 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003462 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003463 self.set_description([
3464 '# Enter a description of the change.',
3465 '# This will be displayed on the codereview site.',
3466 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003467 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003468 '--------------------',
3469 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003470
agable@chromium.org42c20792013-09-12 17:34:49 +00003471 regexp = re.compile(self.BUG_LINE)
3472 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003473 prefix = settings.GetBugPrefix()
3474 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003475 if git_footer:
3476 self.append_footer('Bug: %s' % ', '.join(values))
3477 else:
3478 for value in values:
3479 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003480
agable@chromium.org42c20792013-09-12 17:34:49 +00003481 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003482 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003483 if not content:
3484 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003485 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003486
Bruce Dawson2377b012018-01-11 16:46:49 -08003487 # Strip off comments and default inserted "Bug:" line.
3488 clean_lines = [line.rstrip() for line in lines if not
3489 (line.startswith('#') or line.rstrip() == "Bug:")]
agable@chromium.org42c20792013-09-12 17:34:49 +00003490 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003491 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003492 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003493
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003494 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003495 """Adds a footer line to the description.
3496
3497 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3498 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3499 that Gerrit footers are always at the end.
3500 """
3501 parsed_footer_line = git_footers.parse_footer(line)
3502 if parsed_footer_line:
3503 # Line is a gerrit footer in the form: Footer-Key: any value.
3504 # Thus, must be appended observing Gerrit footer rules.
3505 self.set_description(
3506 git_footers.add_footer(self.description,
3507 key=parsed_footer_line[0],
3508 value=parsed_footer_line[1]))
3509 return
3510
3511 if not self._description_lines:
3512 self._description_lines.append(line)
3513 return
3514
3515 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3516 if gerrit_footers:
3517 # git_footers.split_footers ensures that there is an empty line before
3518 # actual (gerrit) footers, if any. We have to keep it that way.
3519 assert top_lines and top_lines[-1] == ''
3520 top_lines, separator = top_lines[:-1], top_lines[-1:]
3521 else:
3522 separator = [] # No need for separator if there are no gerrit_footers.
3523
3524 prev_line = top_lines[-1] if top_lines else ''
3525 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3526 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3527 top_lines.append('')
3528 top_lines.append(line)
3529 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003530
tandrii99a72f22016-08-17 14:33:24 -07003531 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003532 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003533 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003534 reviewers = [match.group(2).strip()
3535 for match in matches
3536 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003537 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003538
bradnelsond975b302016-10-23 12:20:23 -07003539 def get_cced(self):
3540 """Retrieves the list of reviewers."""
3541 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3542 cced = [match.group(2).strip() for match in matches if match]
3543 return cleanup_list(cced)
3544
Nodir Turakulov23b82142017-11-16 11:04:25 -08003545 def get_hash_tags(self):
3546 """Extracts and sanitizes a list of Gerrit hashtags."""
3547 subject = (self._description_lines or ('',))[0]
3548 subject = re.sub(
3549 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3550
3551 tags = []
3552 start = 0
3553 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3554 while True:
3555 m = bracket_exp.match(subject, start)
3556 if not m:
3557 break
3558 tags.append(self.sanitize_hash_tag(m.group(1)))
3559 start = m.end()
3560
3561 if not tags:
3562 # Try "Tag: " prefix.
3563 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3564 if m:
3565 tags.append(self.sanitize_hash_tag(m.group(1)))
3566 return tags
3567
3568 @classmethod
3569 def sanitize_hash_tag(cls, tag):
3570 """Returns a sanitized Gerrit hash tag.
3571
3572 A sanitized hashtag can be used as a git push refspec parameter value.
3573 """
3574 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3575
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003576 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3577 """Updates this commit description given the parent.
3578
3579 This is essentially what Gnumbd used to do.
3580 Consult https://goo.gl/WMmpDe for more details.
3581 """
3582 assert parent_msg # No, orphan branch creation isn't supported.
3583 assert parent_hash
3584 assert dest_ref
3585 parent_footer_map = git_footers.parse_footers(parent_msg)
3586 # This will also happily parse svn-position, which GnumbD is no longer
3587 # supporting. While we'd generate correct footers, the verifier plugin
3588 # installed in Gerrit will block such commit (ie git push below will fail).
3589 parent_position = git_footers.get_position(parent_footer_map)
3590
3591 # Cherry-picks may have last line obscuring their prior footers,
3592 # from git_footers perspective. This is also what Gnumbd did.
3593 cp_line = None
3594 if (self._description_lines and
3595 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3596 cp_line = self._description_lines.pop()
3597
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003598 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003599
3600 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3601 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003602 for i, line in enumerate(footer_lines):
3603 k, v = git_footers.parse_footer(line) or (None, None)
3604 if k and k.startswith('Cr-'):
3605 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003606
3607 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003608 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003609 if parent_position[0] == dest_ref:
3610 # Same branch as parent.
3611 number = int(parent_position[1]) + 1
3612 else:
3613 number = 1 # New branch, and extra lineage.
3614 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3615 int(parent_position[1])))
3616
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003617 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3618 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003619
3620 self._description_lines = top_lines
3621 if cp_line:
3622 self._description_lines.append(cp_line)
3623 if self._description_lines[-1] != '':
3624 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003625 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003626
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003627
Aaron Gablea1bab272017-04-11 16:38:18 -07003628def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003629 """Retrieves the reviewers that approved a CL from the issue properties with
3630 messages.
3631
3632 Note that the list may contain reviewers that are not committer, thus are not
3633 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003634
3635 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003636 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003637 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003638 return sorted(
3639 set(
3640 message['sender']
3641 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003642 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003643 )
3644 )
3645
3646
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003647def FindCodereviewSettingsFile(filename='codereview.settings'):
3648 """Finds the given file starting in the cwd and going up.
3649
3650 Only looks up to the top of the repository unless an
3651 'inherit-review-settings-ok' file exists in the root of the repository.
3652 """
3653 inherit_ok_file = 'inherit-review-settings-ok'
3654 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003655 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003656 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3657 root = '/'
3658 while True:
3659 if filename in os.listdir(cwd):
3660 if os.path.isfile(os.path.join(cwd, filename)):
3661 return open(os.path.join(cwd, filename))
3662 if cwd == root:
3663 break
3664 cwd = os.path.dirname(cwd)
3665
3666
3667def LoadCodereviewSettingsFromFile(fileobj):
3668 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003669 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003670
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003671 def SetProperty(name, setting, unset_error_ok=False):
3672 fullname = 'rietveld.' + name
3673 if setting in keyvals:
3674 RunGit(['config', fullname, keyvals[setting]])
3675 else:
3676 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3677
tandrii48df5812016-10-17 03:55:37 -07003678 if not keyvals.get('GERRIT_HOST', False):
3679 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003680 # Only server setting is required. Other settings can be absent.
3681 # In that case, we ignore errors raised during option deletion attempt.
3682 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003683 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003684 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3685 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003686 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003687 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3688 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003689 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003690 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3691 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003692
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003693 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003694 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003695
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003696 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003697 RunGit(['config', 'gerrit.squash-uploads',
3698 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003699
tandrii@chromium.org28253532016-04-14 13:46:56 +00003700 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003701 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003702 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3703
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003704 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003705 # should be of the form
3706 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3707 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003708 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3709 keyvals['ORIGIN_URL_CONFIG']])
3710
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003711
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003712def urlretrieve(source, destination):
3713 """urllib is broken for SSL connections via a proxy therefore we
3714 can't use urllib.urlretrieve()."""
3715 with open(destination, 'w') as f:
3716 f.write(urllib2.urlopen(source).read())
3717
3718
ukai@chromium.org712d6102013-11-27 00:52:58 +00003719def hasSheBang(fname):
3720 """Checks fname is a #! script."""
3721 with open(fname) as f:
3722 return f.read(2).startswith('#!')
3723
3724
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003725# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3726def DownloadHooks(*args, **kwargs):
3727 pass
3728
3729
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003730def DownloadGerritHook(force):
3731 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003732
3733 Args:
3734 force: True to update hooks. False to install hooks if not present.
3735 """
3736 if not settings.GetIsGerrit():
3737 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003738 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003739 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3740 if not os.access(dst, os.X_OK):
3741 if os.path.exists(dst):
3742 if not force:
3743 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003744 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003745 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003746 if not hasSheBang(dst):
3747 DieWithError('Not a script: %s\n'
3748 'You need to download from\n%s\n'
3749 'into .git/hooks/commit-msg and '
3750 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003751 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3752 except Exception:
3753 if os.path.exists(dst):
3754 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003755 DieWithError('\nFailed to download hooks.\n'
3756 'You need to download from\n%s\n'
3757 'into .git/hooks/commit-msg and '
3758 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003759
3760
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003761def GetRietveldCodereviewSettingsInteractively():
3762 """Prompt the user for settings."""
3763 server = settings.GetDefaultServerUrl(error_ok=True)
3764 prompt = 'Rietveld server (host[:port])'
3765 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3766 newserver = ask_for_data(prompt + ':')
3767 if not server and not newserver:
3768 newserver = DEFAULT_SERVER
3769 if newserver:
3770 newserver = gclient_utils.UpgradeToHttps(newserver)
3771 if newserver != server:
3772 RunGit(['config', 'rietveld.server', newserver])
3773
3774 def SetProperty(initial, caption, name, is_url):
3775 prompt = caption
3776 if initial:
3777 prompt += ' ("x" to clear) [%s]' % initial
3778 new_val = ask_for_data(prompt + ':')
3779 if new_val == 'x':
3780 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3781 elif new_val:
3782 if is_url:
3783 new_val = gclient_utils.UpgradeToHttps(new_val)
3784 if new_val != initial:
3785 RunGit(['config', 'rietveld.' + name, new_val])
3786
3787 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3788 SetProperty(settings.GetDefaultPrivateFlag(),
3789 'Private flag (rietveld only)', 'private', False)
3790 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3791 'tree-status-url', False)
3792 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3793 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3794 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3795 'run-post-upload-hook', False)
3796
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003797
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003798class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003799 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003800
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003801 _GOOGLESOURCE = 'googlesource.com'
3802
3803 def __init__(self):
3804 # Cached list of [host, identity, source], where source is either
3805 # .gitcookies or .netrc.
3806 self._all_hosts = None
3807
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003808 def ensure_configured_gitcookies(self):
3809 """Runs checks and suggests fixes to make git use .gitcookies from default
3810 path."""
3811 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3812 configured_path = RunGitSilent(
3813 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003814 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003815 if configured_path:
3816 self._ensure_default_gitcookies_path(configured_path, default)
3817 else:
3818 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003819
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003820 @staticmethod
3821 def _ensure_default_gitcookies_path(configured_path, default_path):
3822 assert configured_path
3823 if configured_path == default_path:
3824 print('git is already configured to use your .gitcookies from %s' %
3825 configured_path)
3826 return
3827
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003828 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003829 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3830 (configured_path, default_path))
3831
3832 if not os.path.exists(configured_path):
3833 print('However, your configured .gitcookies file is missing.')
3834 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3835 action='reconfigure')
3836 RunGit(['config', '--global', 'http.cookiefile', default_path])
3837 return
3838
3839 if os.path.exists(default_path):
3840 print('WARNING: default .gitcookies file already exists %s' %
3841 default_path)
3842 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3843 default_path)
3844
3845 confirm_or_exit('Move existing .gitcookies to default location?',
3846 action='move')
3847 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003848 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003849 print('Moved and reconfigured git to use .gitcookies from %s' %
3850 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003851
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003852 @staticmethod
3853 def _configure_gitcookies_path(default_path):
3854 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3855 if os.path.exists(netrc_path):
3856 print('You seem to be using outdated .netrc for git credentials: %s' %
3857 netrc_path)
3858 print('This tool will guide you through setting up recommended '
3859 '.gitcookies store for git credentials.\n'
3860 '\n'
3861 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3862 ' git config --global --unset http.cookiefile\n'
3863 ' mv %s %s.backup\n\n' % (default_path, default_path))
3864 confirm_or_exit(action='setup .gitcookies')
3865 RunGit(['config', '--global', 'http.cookiefile', default_path])
3866 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003867
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003868 def get_hosts_with_creds(self, include_netrc=False):
3869 if self._all_hosts is None:
3870 a = gerrit_util.CookiesAuthenticator()
3871 self._all_hosts = [
3872 (h, u, s)
3873 for h, u, s in itertools.chain(
3874 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3875 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3876 )
3877 if h.endswith(self._GOOGLESOURCE)
3878 ]
3879
3880 if include_netrc:
3881 return self._all_hosts
3882 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3883
3884 def print_current_creds(self, include_netrc=False):
3885 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3886 if not hosts:
3887 print('No Git/Gerrit credentials found')
3888 return
3889 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3890 header = [('Host', 'User', 'Which file'),
3891 ['=' * l for l in lengths]]
3892 for row in (header + hosts):
3893 print('\t'.join((('%%+%ds' % l) % s)
3894 for l, s in zip(lengths, row)))
3895
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003896 @staticmethod
3897 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003898 """Parses identity "git-<username>.domain" into <username> and domain."""
3899 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003900 # distinguishable from sub-domains. But we do know typical domains:
3901 if identity.endswith('.chromium.org'):
3902 domain = 'chromium.org'
3903 username = identity[:-len('.chromium.org')]
3904 else:
3905 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003906 if username.startswith('git-'):
3907 username = username[len('git-'):]
3908 return username, domain
3909
3910 def _get_usernames_of_domain(self, domain):
3911 """Returns list of usernames referenced by .gitcookies in a given domain."""
3912 identities_by_domain = {}
3913 for _, identity, _ in self.get_hosts_with_creds():
3914 username, domain = self._parse_identity(identity)
3915 identities_by_domain.setdefault(domain, []).append(username)
3916 return identities_by_domain.get(domain)
3917
3918 def _canonical_git_googlesource_host(self, host):
3919 """Normalizes Gerrit hosts (with '-review') to Git host."""
3920 assert host.endswith(self._GOOGLESOURCE)
3921 # Prefix doesn't include '.' at the end.
3922 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3923 if prefix.endswith('-review'):
3924 prefix = prefix[:-len('-review')]
3925 return prefix + '.' + self._GOOGLESOURCE
3926
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003927 def _canonical_gerrit_googlesource_host(self, host):
3928 git_host = self._canonical_git_googlesource_host(host)
3929 prefix = git_host.split('.', 1)[0]
3930 return prefix + '-review.' + self._GOOGLESOURCE
3931
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003932 def _get_counterpart_host(self, host):
3933 assert host.endswith(self._GOOGLESOURCE)
3934 git = self._canonical_git_googlesource_host(host)
3935 gerrit = self._canonical_gerrit_googlesource_host(git)
3936 return git if gerrit == host else gerrit
3937
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003938 def has_generic_host(self):
3939 """Returns whether generic .googlesource.com has been configured.
3940
3941 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3942 """
3943 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3944 if host == '.' + self._GOOGLESOURCE:
3945 return True
3946 return False
3947
3948 def _get_git_gerrit_identity_pairs(self):
3949 """Returns map from canonic host to pair of identities (Git, Gerrit).
3950
3951 One of identities might be None, meaning not configured.
3952 """
3953 host_to_identity_pairs = {}
3954 for host, identity, _ in self.get_hosts_with_creds():
3955 canonical = self._canonical_git_googlesource_host(host)
3956 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3957 idx = 0 if canonical == host else 1
3958 pair[idx] = identity
3959 return host_to_identity_pairs
3960
3961 def get_partially_configured_hosts(self):
3962 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003963 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3964 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3965 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003966
3967 def get_conflicting_hosts(self):
3968 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003969 host
3970 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003971 if None not in (i1, i2) and i1 != i2)
3972
3973 def get_duplicated_hosts(self):
3974 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3975 return set(host for host, count in counters.iteritems() if count > 1)
3976
3977 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3978 'chromium.googlesource.com': 'chromium.org',
3979 'chrome-internal.googlesource.com': 'google.com',
3980 }
3981
3982 def get_hosts_with_wrong_identities(self):
3983 """Finds hosts which **likely** reference wrong identities.
3984
3985 Note: skips hosts which have conflicting identities for Git and Gerrit.
3986 """
3987 hosts = set()
3988 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3989 pair = self._get_git_gerrit_identity_pairs().get(host)
3990 if pair and pair[0] == pair[1]:
3991 _, domain = self._parse_identity(pair[0])
3992 if domain != expected:
3993 hosts.add(host)
3994 return hosts
3995
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003996 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003997 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003998 hosts = sorted(hosts)
3999 assert hosts
4000 if extra_column_func is None:
4001 extras = [''] * len(hosts)
4002 else:
4003 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004004 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
4005 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004006 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004007 lines.append(tmpl % he)
4008 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004009
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004010 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004011 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004012 yield ('.googlesource.com wildcard record detected',
4013 ['Chrome Infrastructure team recommends to list full host names '
4014 'explicitly.'],
4015 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004016
4017 dups = self.get_duplicated_hosts()
4018 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004019 yield ('The following hosts were defined twice',
4020 self._format_hosts(dups),
4021 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004022
4023 partial = self.get_partially_configured_hosts()
4024 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004025 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
4026 'These hosts are missing',
4027 self._format_hosts(partial, lambda host: 'but %s defined' %
4028 self._get_counterpart_host(host)),
4029 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004030
4031 conflicting = self.get_conflicting_hosts()
4032 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004033 yield ('The following Git hosts have differing credentials from their '
4034 'Gerrit counterparts',
4035 self._format_hosts(conflicting, lambda host: '%s vs %s' %
4036 tuple(self._get_git_gerrit_identity_pairs()[host])),
4037 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004038
4039 wrong = self.get_hosts_with_wrong_identities()
4040 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004041 yield ('These hosts likely use wrong identity',
4042 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
4043 (self._get_git_gerrit_identity_pairs()[host][0],
4044 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
4045 wrong)
4046
4047 def find_and_report_problems(self):
4048 """Returns True if there was at least one problem, else False."""
4049 found = False
4050 bad_hosts = set()
4051 for title, sublines, hosts in self._find_problems():
4052 if not found:
4053 found = True
4054 print('\n\n.gitcookies problem report:\n')
4055 bad_hosts.update(hosts or [])
4056 print(' %s%s' % (title , (':' if sublines else '')))
4057 if sublines:
4058 print()
4059 print(' %s' % '\n '.join(sublines))
4060 print()
4061
4062 if bad_hosts:
4063 assert found
4064 print(' You can manually remove corresponding lines in your %s file and '
4065 'visit the following URLs with correct account to generate '
4066 'correct credential lines:\n' %
4067 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
4068 print(' %s' % '\n '.join(sorted(set(
4069 gerrit_util.CookiesAuthenticator().get_new_password_url(
4070 self._canonical_git_googlesource_host(host))
4071 for host in bad_hosts
4072 ))))
4073 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004074
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004075
4076def CMDcreds_check(parser, args):
4077 """Checks credentials and suggests changes."""
4078 _, _ = parser.parse_args(args)
4079
4080 if gerrit_util.GceAuthenticator.is_gce():
Aaron Gabled10ca0e2017-09-11 11:24:10 -07004081 DieWithError(
4082 'This command is not designed for GCE, are you on a bot?\n'
4083 'If you need to run this, export SKIP_GCE_AUTH_FOR_GIT=1 in your env.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004084
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01004085 checker = _GitCookiesChecker()
4086 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004087
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004088 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004089 checker.print_current_creds(include_netrc=True)
4090
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004091 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004092 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004093 return 0
4094 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004095
4096
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004097@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004098def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004099 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004100
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004101 print('WARNING: git cl config works for Rietveld only.')
tandrii5d0a0422016-09-14 06:24:35 -07004102 # TODO(tandrii): remove this once we switch to Gerrit.
4103 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00004104 parser.add_option('--activate-update', action='store_true',
4105 help='activate auto-updating [rietveld] section in '
4106 '.git/config')
4107 parser.add_option('--deactivate-update', action='store_true',
4108 help='deactivate auto-updating [rietveld] section in '
4109 '.git/config')
4110 options, args = parser.parse_args(args)
4111
4112 if options.deactivate_update:
4113 RunGit(['config', 'rietveld.autoupdate', 'false'])
4114 return
4115
4116 if options.activate_update:
4117 RunGit(['config', '--unset', 'rietveld.autoupdate'])
4118 return
4119
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004120 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00004121 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004122 return 0
4123
4124 url = args[0]
4125 if not url.endswith('codereview.settings'):
4126 url = os.path.join(url, 'codereview.settings')
4127
4128 # Load code review settings and download hooks (if available).
4129 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
4130 return 0
4131
4132
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004133def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004134 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004135 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
4136 branch = ShortBranchName(branchref)
4137 _, args = parser.parse_args(args)
4138 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07004139 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004140 return RunGit(['config', 'branch.%s.base-url' % branch],
4141 error_ok=False).strip()
4142 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004143 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004144 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
4145 error_ok=False).strip()
4146
4147
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004148def color_for_status(status):
4149 """Maps a Changelist status to color, for CMDstatus and other tools."""
4150 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07004151 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004152 'waiting': Fore.BLUE,
4153 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07004154 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004155 'lgtm': Fore.GREEN,
4156 'commit': Fore.MAGENTA,
4157 'closed': Fore.CYAN,
4158 'error': Fore.WHITE,
4159 }.get(status, Fore.WHITE)
4160
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00004161
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004162def get_cl_statuses(changes, fine_grained, max_processes=None):
4163 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004164
4165 If fine_grained is true, this will fetch CL statuses from the server.
4166 Otherwise, simply indicate if there's a matching url for the given branches.
4167
4168 If max_processes is specified, it is used as the maximum number of processes
4169 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
4170 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004171
4172 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004173 """
qyearsley12fa6ff2016-08-24 09:18:40 -07004174 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004175 upload.verbosity = 0
4176
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004177 if not changes:
4178 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004179
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004180 if not fine_grained:
4181 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07004182 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004183 for cl in changes:
4184 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004185 return
4186
4187 # First, sort out authentication issues.
4188 logging.debug('ensuring credentials exist')
4189 for cl in changes:
4190 cl.EnsureAuthenticated(force=False, refresh=True)
4191
4192 def fetch(cl):
4193 try:
4194 return (cl, cl.GetStatus())
4195 except:
4196 # See http://crbug.com/629863.
4197 logging.exception('failed to fetch status for %s:', cl)
4198 raise
4199
4200 threads_count = len(changes)
4201 if max_processes:
4202 threads_count = max(1, min(threads_count, max_processes))
4203 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
4204
4205 pool = ThreadPool(threads_count)
4206 fetched_cls = set()
4207 try:
4208 it = pool.imap_unordered(fetch, changes).__iter__()
4209 while True:
4210 try:
4211 cl, status = it.next(timeout=5)
4212 except multiprocessing.TimeoutError:
4213 break
4214 fetched_cls.add(cl)
4215 yield cl, status
4216 finally:
4217 pool.close()
4218
4219 # Add any branches that failed to fetch.
4220 for cl in set(changes) - fetched_cls:
4221 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004222
rmistry@google.com2dd99862015-06-22 12:22:18 +00004223
4224def upload_branch_deps(cl, args):
4225 """Uploads CLs of local branches that are dependents of the current branch.
4226
4227 If the local branch dependency tree looks like:
4228 test1 -> test2.1 -> test3.1
4229 -> test3.2
4230 -> test2.2 -> test3.3
4231
4232 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
4233 run on the dependent branches in this order:
4234 test2.1, test3.1, test3.2, test2.2, test3.3
4235
4236 Note: This function does not rebase your local dependent branches. Use it when
4237 you make a change to the parent branch that will not conflict with its
4238 dependent branches, and you would like their dependencies updated in
4239 Rietveld.
4240 """
4241 if git_common.is_dirty_git_tree('upload-branch-deps'):
4242 return 1
4243
4244 root_branch = cl.GetBranch()
4245 if root_branch is None:
4246 DieWithError('Can\'t find dependent branches from detached HEAD state. '
4247 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01004248 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00004249 DieWithError('Current branch does not have an uploaded CL. We cannot set '
4250 'patchset dependencies without an uploaded CL.')
4251
4252 branches = RunGit(['for-each-ref',
4253 '--format=%(refname:short) %(upstream:short)',
4254 'refs/heads'])
4255 if not branches:
4256 print('No local branches found.')
4257 return 0
4258
4259 # Create a dictionary of all local branches to the branches that are dependent
4260 # on it.
4261 tracked_to_dependents = collections.defaultdict(list)
4262 for b in branches.splitlines():
4263 tokens = b.split()
4264 if len(tokens) == 2:
4265 branch_name, tracked = tokens
4266 tracked_to_dependents[tracked].append(branch_name)
4267
vapiera7fbd5a2016-06-16 09:17:49 -07004268 print()
4269 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004270 dependents = []
4271 def traverse_dependents_preorder(branch, padding=''):
4272 dependents_to_process = tracked_to_dependents.get(branch, [])
4273 padding += ' '
4274 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07004275 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00004276 dependents.append(dependent)
4277 traverse_dependents_preorder(dependent, padding)
4278 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07004279 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004280
4281 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004282 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004283 return 0
4284
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01004285 confirm_or_exit('This command will checkout all dependent branches and run '
4286 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004287
andybons@chromium.org962f9462016-02-03 20:00:42 +00004288 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00004289 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00004290 args.extend(['-t', 'Updated patchset dependency'])
4291
rmistry@google.com2dd99862015-06-22 12:22:18 +00004292 # Record all dependents that failed to upload.
4293 failures = {}
4294 # Go through all dependents, checkout the branch and upload.
4295 try:
4296 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004297 print()
4298 print('--------------------------------------')
4299 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004300 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07004301 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004302 try:
4303 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004304 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004305 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004306 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00004307 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07004308 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004309 finally:
4310 # Swap back to the original root branch.
4311 RunGit(['checkout', '-q', root_branch])
4312
vapiera7fbd5a2016-06-16 09:17:49 -07004313 print()
4314 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004315 for dependent_branch in dependents:
4316 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07004317 print(' %s : %s' % (dependent_branch, upload_status))
4318 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004319
4320 return 0
4321
4322
kmarshall3bff56b2016-06-06 18:31:47 -07004323def CMDarchive(parser, args):
4324 """Archives and deletes branches associated with closed changelists."""
4325 parser.add_option(
4326 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07004327 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07004328 parser.add_option(
4329 '-f', '--force', action='store_true',
4330 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07004331 parser.add_option(
4332 '-d', '--dry-run', action='store_true',
4333 help='Skip the branch tagging and removal steps.')
4334 parser.add_option(
4335 '-t', '--notags', action='store_true',
4336 help='Do not tag archived branches. '
4337 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004338
4339 auth.add_auth_options(parser)
4340 options, args = parser.parse_args(args)
4341 if args:
4342 parser.error('Unsupported args: %s' % ' '.join(args))
4343 auth_config = auth.extract_auth_config_from_options(options)
4344
4345 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4346 if not branches:
4347 return 0
4348
vapiera7fbd5a2016-06-16 09:17:49 -07004349 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004350 changes = [Changelist(branchref=b, auth_config=auth_config)
4351 for b in branches.splitlines()]
4352 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4353 statuses = get_cl_statuses(changes,
4354 fine_grained=True,
4355 max_processes=options.maxjobs)
4356 proposal = [(cl.GetBranch(),
4357 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4358 for cl, status in statuses
4359 if status == 'closed']
4360 proposal.sort()
4361
4362 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004363 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004364 return 0
4365
4366 current_branch = GetCurrentBranch()
4367
vapiera7fbd5a2016-06-16 09:17:49 -07004368 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004369 if options.notags:
4370 for next_item in proposal:
4371 print(' ' + next_item[0])
4372 else:
4373 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4374 for next_item in proposal:
4375 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004376
kmarshall9249e012016-08-23 12:02:16 -07004377 # Quit now on precondition failure or if instructed by the user, either
4378 # via an interactive prompt or by command line flags.
4379 if options.dry_run:
4380 print('\nNo changes were made (dry run).\n')
4381 return 0
4382 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004383 print('You are currently on a branch \'%s\' which is associated with a '
4384 'closed codereview issue, so archive cannot proceed. Please '
4385 'checkout another branch and run this command again.' %
4386 current_branch)
4387 return 1
kmarshall9249e012016-08-23 12:02:16 -07004388 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004389 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4390 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004391 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004392 return 1
4393
4394 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004395 if not options.notags:
4396 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004397 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004398
vapiera7fbd5a2016-06-16 09:17:49 -07004399 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004400
4401 return 0
4402
4403
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004404def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004405 """Show status of changelists.
4406
4407 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004408 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004409 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004410 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004411 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004412 - Magenta in the commit queue
4413 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004414 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004415
4416 Also see 'git cl comments'.
4417 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004418 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004419 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004420 parser.add_option('-f', '--fast', action='store_true',
4421 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004422 parser.add_option(
4423 '-j', '--maxjobs', action='store', type=int,
4424 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004425
4426 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004427 _add_codereview_issue_select_options(
4428 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004429 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004430 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004431 if args:
4432 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004433 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004434
iannuccie53c9352016-08-17 14:40:40 -07004435 if options.issue is not None and not options.field:
4436 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004437
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004438 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004439 cl = Changelist(auth_config=auth_config, issue=options.issue,
4440 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004441 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004442 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004443 elif options.field == 'id':
4444 issueid = cl.GetIssue()
4445 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004446 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004447 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004448 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004449 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004450 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004451 elif options.field == 'status':
4452 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004453 elif options.field == 'url':
4454 url = cl.GetIssueURL()
4455 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004456 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004457 return 0
4458
4459 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4460 if not branches:
4461 print('No local branch found.')
4462 return 0
4463
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004464 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004465 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004466 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004467 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004468 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004469 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004470 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004471
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004472 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004473 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
4474 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4475 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004476 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004477 c, status = output.next()
4478 branch_statuses[c.GetBranch()] = status
4479 status = branch_statuses.pop(branch)
4480 url = cl.GetIssueURL()
4481 if url and (not status or status == 'error'):
4482 # The issue probably doesn't exist anymore.
4483 url += ' (broken)'
4484
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004485 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004486 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004487 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004488 color = ''
4489 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004490 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07004491 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004492 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07004493 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004494
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004495
4496 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07004497 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004498 print('Current branch: %s' % branch)
4499 for cl in changes:
4500 if cl.GetBranch() == branch:
4501 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004502 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004503 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004504 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004505 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004506 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004507 print('Issue description:')
4508 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004509 return 0
4510
4511
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004512def colorize_CMDstatus_doc():
4513 """To be called once in main() to add colors to git cl status help."""
4514 colors = [i for i in dir(Fore) if i[0].isupper()]
4515
4516 def colorize_line(line):
4517 for color in colors:
4518 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004519 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004520 indent = len(line) - len(line.lstrip(' ')) + 1
4521 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4522 return line
4523
4524 lines = CMDstatus.__doc__.splitlines()
4525 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4526
4527
phajdan.jre328cf92016-08-22 04:12:17 -07004528def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004529 if path == '-':
4530 json.dump(contents, sys.stdout)
4531 else:
4532 with open(path, 'w') as f:
4533 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004534
4535
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004536@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004537def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004538 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004539
4540 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004541 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004542 parser.add_option('-r', '--reverse', action='store_true',
4543 help='Lookup the branch(es) for the specified issues. If '
4544 'no issues are specified, all branches with mapped '
4545 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004546 parser.add_option('--json',
4547 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004548 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004549 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004550 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004551
dnj@chromium.org406c4402015-03-03 17:22:28 +00004552 if options.reverse:
4553 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004554 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004555 # Reverse issue lookup.
4556 issue_branch_map = {}
4557 for branch in branches:
4558 cl = Changelist(branchref=branch)
4559 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
4560 if not args:
4561 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004562 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004563 for issue in args:
4564 if not issue:
4565 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004566 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004567 print('Branch for issue number %s: %s' % (
4568 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004569 if options.json:
4570 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004571 return 0
4572
4573 if len(args) > 0:
4574 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4575 if not issue.valid:
4576 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4577 'or no argument to list it.\n'
4578 'Maybe you want to run git cl status?')
4579 cl = Changelist(codereview=issue.codereview)
4580 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004581 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004582 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004583 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4584 if options.json:
4585 write_json(options.json, {
4586 'issue': cl.GetIssue(),
4587 'issue_url': cl.GetIssueURL(),
4588 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004589 return 0
4590
4591
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004592def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004593 """Shows or posts review comments for any changelist."""
4594 parser.add_option('-a', '--add-comment', dest='comment',
4595 help='comment to add to an issue')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004596 parser.add_option('-i', '--issue', dest='issue',
4597 help='review issue id (defaults to current issue). '
4598 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004599 parser.add_option('-m', '--machine-readable', dest='readable',
4600 action='store_false', default=True,
4601 help='output comments in a format compatible with '
4602 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004603 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004604 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004605 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004606 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004607 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004608 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004609 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004610
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004611 issue = None
4612 if options.issue:
4613 try:
4614 issue = int(options.issue)
4615 except ValueError:
4616 DieWithError('A review issue id is expected to be a number')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004617 if not options.forced_codereview:
4618 parser.error('--gerrit or --rietveld is required if --issue is specified')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004619
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004620 cl = Changelist(issue=issue,
Andrii Shyshkalov70909e12017-04-10 14:38:32 +02004621 codereview=options.forced_codereview,
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004622 auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004623
4624 if options.comment:
4625 cl.AddComment(options.comment)
4626 return 0
4627
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004628 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4629 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004630 for comment in summary:
4631 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004632 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004633 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004634 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004635 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004636 color = Fore.MAGENTA
4637 else:
4638 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004639 print('\n%s%s %s%s\n%s' % (
4640 color,
4641 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4642 comment.sender,
4643 Fore.RESET,
4644 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4645
smut@google.comc85ac942015-09-15 16:34:43 +00004646 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004647 def pre_serialize(c):
4648 dct = c.__dict__.copy()
4649 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4650 return dct
smut@google.comc85ac942015-09-15 16:34:43 +00004651 with open(options.json_file, 'wb') as f:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004652 json.dump(map(pre_serialize, summary), f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004653 return 0
4654
4655
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004656@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004657def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004658 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004659 parser.add_option('-d', '--display', action='store_true',
4660 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004661 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004662 help='New description to set for this issue (- for stdin, '
4663 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004664 parser.add_option('-f', '--force', action='store_true',
4665 help='Delete any unpublished Gerrit edits for this issue '
4666 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004667
4668 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004669 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004670 options, args = parser.parse_args(args)
4671 _process_codereview_select_options(parser, options)
4672
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004673 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004674 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004675 target_issue_arg = ParseIssueNumberArgument(args[0],
4676 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004677 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004678 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004679
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004680 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004681
martiniss6eda05f2016-06-30 10:18:35 -07004682 kwargs = {
4683 'auth_config': auth_config,
4684 'codereview': options.forced_codereview,
4685 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004686 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004687 if target_issue_arg:
4688 kwargs['issue'] = target_issue_arg.issue
4689 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004690 if target_issue_arg.codereview and not options.forced_codereview:
4691 detected_codereview_from_url = True
4692 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004693
4694 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004695 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004696 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004697 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004698
4699 if detected_codereview_from_url:
4700 logging.info('canonical issue/change URL: %s (type: %s)\n',
4701 cl.GetIssueURL(), target_issue_arg.codereview)
4702
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004703 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004704
smut@google.com34fb6b12015-07-13 20:03:26 +00004705 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004706 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004707 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004708
4709 if options.new_description:
4710 text = options.new_description
4711 if text == '-':
4712 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004713 elif text == '+':
4714 base_branch = cl.GetCommonAncestorWithUpstream()
4715 change = cl.GetChange(base_branch, None, local_description=True)
4716 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004717
4718 description.set_description(text)
4719 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004720 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004721
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004722 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004723 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004724 return 0
4725
4726
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004727def CreateDescriptionFromLog(args):
4728 """Pulls out the commit log to use as a base for the CL description."""
4729 log_args = []
4730 if len(args) == 1 and not args[0].endswith('.'):
4731 log_args = [args[0] + '..']
4732 elif len(args) == 1 and args[0].endswith('...'):
4733 log_args = [args[0][:-1]]
4734 elif len(args) == 2:
4735 log_args = [args[0] + '..' + args[1]]
4736 else:
4737 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004738 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004739
4740
thestig@chromium.org44202a22014-03-11 19:22:18 +00004741def CMDlint(parser, args):
4742 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004743 parser.add_option('--filter', action='append', metavar='-x,+y',
4744 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004745 auth.add_auth_options(parser)
4746 options, args = parser.parse_args(args)
4747 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004748
4749 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004750 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004751 try:
4752 import cpplint
4753 import cpplint_chromium
4754 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004755 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004756 return 1
4757
4758 # Change the current working directory before calling lint so that it
4759 # shows the correct base.
4760 previous_cwd = os.getcwd()
4761 os.chdir(settings.GetRoot())
4762 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004763 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004764 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4765 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004766 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004767 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004768 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004769
4770 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004771 command = args + files
4772 if options.filter:
4773 command = ['--filter=' + ','.join(options.filter)] + command
4774 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004775
4776 white_regex = re.compile(settings.GetLintRegex())
4777 black_regex = re.compile(settings.GetLintIgnoreRegex())
4778 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4779 for filename in filenames:
4780 if white_regex.match(filename):
4781 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004782 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004783 else:
4784 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4785 extra_check_functions)
4786 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004787 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004788 finally:
4789 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004790 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004791 if cpplint._cpplint_state.error_count != 0:
4792 return 1
4793 return 0
4794
4795
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004796def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004797 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004798 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004799 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004800 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004801 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004802 parser.add_option('--all', action='store_true',
4803 help='Run checks against all files, not just modified ones')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004804 auth.add_auth_options(parser)
4805 options, args = parser.parse_args(args)
4806 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004807
sbc@chromium.org71437c02015-04-09 19:29:40 +00004808 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004809 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004810 return 1
4811
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004812 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004813 if args:
4814 base_branch = args[0]
4815 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004816 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004817 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004818
Aaron Gable8076c282017-11-29 14:39:41 -08004819 if options.all:
4820 base_change = cl.GetChange(base_branch, None)
4821 files = [('M', f) for f in base_change.AllFiles()]
4822 change = presubmit_support.GitChange(
4823 base_change.Name(),
4824 base_change.FullDescriptionText(),
4825 base_change.RepositoryRoot(),
4826 files,
4827 base_change.issue,
4828 base_change.patchset,
4829 base_change.author_email,
4830 base_change._upstream)
4831 else:
4832 change = cl.GetChange(base_branch, None)
4833
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004834 cl.RunHook(
4835 committing=not options.upload,
4836 may_prompt=False,
4837 verbose=options.verbose,
Aaron Gable8076c282017-11-29 14:39:41 -08004838 change=change)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004839 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004840
4841
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004842def GenerateGerritChangeId(message):
4843 """Returns Ixxxxxx...xxx change id.
4844
4845 Works the same way as
4846 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4847 but can be called on demand on all platforms.
4848
4849 The basic idea is to generate git hash of a state of the tree, original commit
4850 message, author/committer info and timestamps.
4851 """
4852 lines = []
4853 tree_hash = RunGitSilent(['write-tree'])
4854 lines.append('tree %s' % tree_hash.strip())
4855 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4856 if code == 0:
4857 lines.append('parent %s' % parent.strip())
4858 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4859 lines.append('author %s' % author.strip())
4860 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4861 lines.append('committer %s' % committer.strip())
4862 lines.append('')
4863 # Note: Gerrit's commit-hook actually cleans message of some lines and
4864 # whitespace. This code is not doing this, but it clearly won't decrease
4865 # entropy.
4866 lines.append(message)
4867 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4868 stdin='\n'.join(lines))
4869 return 'I%s' % change_hash.strip()
4870
4871
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004872def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004873 """Computes the remote branch ref to use for the CL.
4874
4875 Args:
4876 remote (str): The git remote for the CL.
4877 remote_branch (str): The git remote branch for the CL.
4878 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004879 """
4880 if not (remote and remote_branch):
4881 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004882
wittman@chromium.org455dc922015-01-26 20:15:50 +00004883 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004884 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004885 # refs, which are then translated into the remote full symbolic refs
4886 # below.
4887 if '/' not in target_branch:
4888 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4889 else:
4890 prefix_replacements = (
4891 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4892 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4893 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4894 )
4895 match = None
4896 for regex, replacement in prefix_replacements:
4897 match = re.search(regex, target_branch)
4898 if match:
4899 remote_branch = target_branch.replace(match.group(0), replacement)
4900 break
4901 if not match:
4902 # This is a branch path but not one we recognize; use as-is.
4903 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004904 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4905 # Handle the refs that need to land in different refs.
4906 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004907
wittman@chromium.org455dc922015-01-26 20:15:50 +00004908 # Create the true path to the remote branch.
4909 # Does the following translation:
4910 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4911 # * refs/remotes/origin/master -> refs/heads/master
4912 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4913 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4914 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4915 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4916 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4917 'refs/heads/')
4918 elif remote_branch.startswith('refs/remotes/branch-heads'):
4919 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004920
wittman@chromium.org455dc922015-01-26 20:15:50 +00004921 return remote_branch
4922
4923
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004924def cleanup_list(l):
4925 """Fixes a list so that comma separated items are put as individual items.
4926
4927 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4928 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4929 """
4930 items = sum((i.split(',') for i in l), [])
4931 stripped_items = (i.strip() for i in items)
4932 return sorted(filter(None, stripped_items))
4933
4934
Aaron Gable4db38df2017-11-03 14:59:07 -07004935@subcommand.usage('[flags]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004936def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004937 """Uploads the current changelist to codereview.
4938
4939 Can skip dependency patchset uploads for a branch by running:
4940 git config branch.branch_name.skip-deps-uploads True
4941 To unset run:
4942 git config --unset branch.branch_name.skip-deps-uploads
4943 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004944
4945 If the name of the checked out branch starts with "bug-" or "fix-" followed by
4946 a bug number, this bug number is automatically populated in the CL
4947 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004948
4949 If subject contains text in square brackets or has "<text>: " prefix, such
4950 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
4951 [git-cl] add support for hashtags
4952 Foo bar: implement foo
4953 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004954 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004955 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4956 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004957 parser.add_option('--bypass-watchlists', action='store_true',
4958 dest='bypass_watchlists',
4959 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004960 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004961 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004962 parser.add_option('--message', '-m', dest='message',
4963 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004964 parser.add_option('-b', '--bug',
4965 help='pre-populate the bug number(s) for this issue. '
4966 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004967 parser.add_option('--message-file', dest='message_file',
4968 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004969 parser.add_option('--title', '-t', dest='title',
4970 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004971 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004972 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004973 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004974 parser.add_option('--tbrs',
4975 action='append', default=[],
4976 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004977 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004978 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004979 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004980 parser.add_option('--hashtag', dest='hashtags',
4981 action='append', default=[],
4982 help=('Gerrit hashtag for new CL; '
4983 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004984 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004985 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004986 parser.add_option('--emulate_svn_auto_props',
4987 '--emulate-svn-auto-props',
4988 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004989 dest="emulate_svn_auto_props",
4990 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004991 parser.add_option('-c', '--use-commit-queue', action='store_true',
Aaron Gableedbc4132017-09-11 13:22:28 -07004992 help='tell the commit queue to commit this patchset; '
4993 'implies --send-mail')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004994 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004995 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004996 metavar='TARGET',
4997 help='Apply CL to remote ref TARGET. ' +
4998 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004999 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005000 help='Squash multiple commits into one')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00005001 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005002 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07005003 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005004 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07005005 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
5006 const='TBR', help='add a set of OWNERS to TBR')
5007 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
5008 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00005009 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
5010 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00005011 help='Send the patchset to do a CQ dry run right after '
5012 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00005013 parser.add_option('--dependencies', action='store_true',
5014 help='Uploads CLs of all the local branches that depend on '
5015 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00005016
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005017 # TODO: remove Rietveld flags
5018 parser.add_option('--private', action='store_true',
5019 help='set the review private (rietveld only)')
5020 parser.add_option('--email', default=None,
5021 help='email address to use to connect to Rietveld')
5022
rmistry@google.com2dd99862015-06-22 12:22:18 +00005023 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00005024 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005025 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005026 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005027 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005028 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005029 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005030
sbc@chromium.org71437c02015-04-09 19:29:40 +00005031 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00005032 return 1
5033
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005034 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07005035 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005036 options.cc = cleanup_list(options.cc)
5037
tandriib80458a2016-06-23 12:20:07 -07005038 if options.message_file:
5039 if options.message:
5040 parser.error('only one of --message and --message-file allowed.')
5041 options.message = gclient_utils.FileRead(options.message_file)
5042 options.message_file = None
5043
tandrii4d0545a2016-07-06 03:56:49 -07005044 if options.cq_dry_run and options.use_commit_queue:
5045 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
5046
Aaron Gableedbc4132017-09-11 13:22:28 -07005047 if options.use_commit_queue:
5048 options.send_mail = True
5049
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00005050 # For sanity of test expectations, do this otherwise lazy-loading *now*.
5051 settings.GetIsGerrit()
5052
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005053 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00005054 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005055
5056
Francois Dorayd42c6812017-05-30 15:10:20 -04005057@subcommand.usage('--description=<description file>')
5058def CMDsplit(parser, args):
5059 """Splits a branch into smaller branches and uploads CLs.
5060
5061 Creates a branch and uploads a CL for each group of files modified in the
5062 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07005063 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04005064 the shared OWNERS file.
5065 """
5066 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05005067 help="A text file containing a CL description in which "
5068 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005069 parser.add_option("-c", "--comment", dest="comment_file",
5070 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11005071 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
5072 default=False,
5073 help="List the files and reviewers for each CL that would "
5074 "be created, but don't create branches or CLs.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005075 options, _ = parser.parse_args(args)
5076
5077 if not options.description_file:
5078 parser.error('No --description flag specified.')
5079
5080 def WrappedCMDupload(args):
5081 return CMDupload(OptionParser(), args)
5082
5083 return split_cl.SplitCl(options.description_file, options.comment_file,
Chris Watkinsba28e462017-12-13 11:22:17 +11005084 Changelist, WrappedCMDupload, options.dry_run)
Francois Dorayd42c6812017-05-30 15:10:20 -04005085
5086
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005087@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005088def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005089 """DEPRECATED: Used to commit the current changelist via git-svn."""
5090 message = ('git-cl no longer supports committing to SVN repositories via '
5091 'git-svn. You probably want to use `git cl land` instead.')
5092 print(message)
5093 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005094
5095
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005096# Two special branches used by git cl land.
5097MERGE_BRANCH = 'git-cl-commit'
5098CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
5099
5100
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005101@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00005102def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005103 """Commits the current changelist via git.
5104
5105 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
5106 upstream and closes the issue automatically and atomically.
5107
5108 Otherwise (in case of Rietveld):
5109 Squashes branch into a single commit.
5110 Updates commit message with metadata (e.g. pointer to review).
5111 Pushes the code upstream.
5112 Updates review and closes.
5113 """
5114 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
5115 help='bypass upload presubmit hook')
5116 parser.add_option('-m', dest='message',
5117 help="override review description")
Aaron Gablef7543cd2017-07-20 14:26:31 -07005118 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005119 help="force yes to questions (don't prompt)")
5120 parser.add_option('-c', dest='contributor',
5121 help="external contributor for patch (appended to " +
5122 "description and used as author for git). Should be " +
5123 "formatted as 'First Last <email@example.com>'")
5124 add_git_similarity(parser)
5125 auth.add_auth_options(parser)
5126 (options, args) = parser.parse_args(args)
5127 auth_config = auth.extract_auth_config_from_options(options)
5128
5129 cl = Changelist(auth_config=auth_config)
5130
5131 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
5132 if cl.IsGerrit():
5133 if options.message:
5134 # This could be implemented, but it requires sending a new patch to
5135 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
5136 # Besides, Gerrit has the ability to change the commit message on submit
5137 # automatically, thus there is no need to support this option (so far?).
5138 parser.error('-m MESSAGE option is not supported for Gerrit.')
5139 if options.contributor:
5140 parser.error(
5141 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
5142 'Before uploading a commit to Gerrit, ensure it\'s author field is '
5143 'the contributor\'s "name <email>". If you can\'t upload such a '
5144 'commit for review, contact your repository admin and request'
5145 '"Forge-Author" permission.')
5146 if not cl.GetIssue():
5147 DieWithError('You must upload the change first to Gerrit.\n'
5148 ' If you would rather have `git cl land` upload '
5149 'automatically for you, see http://crbug.com/642759')
5150 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
5151 options.verbose)
5152
5153 current = cl.GetBranch()
5154 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
5155 if remote == '.':
5156 print()
5157 print('Attempting to push branch %r into another local branch!' % current)
5158 print()
5159 print('Either reparent this branch on top of origin/master:')
5160 print(' git reparent-branch --root')
5161 print()
5162 print('OR run `git rebase-update` if you think the parent branch is ')
5163 print('already committed.')
5164 print()
5165 print(' Current parent: %r' % upstream_branch)
5166 return 1
5167
5168 if not args:
5169 # Default to merging against our best guess of the upstream branch.
5170 args = [cl.GetUpstreamBranch()]
5171
5172 if options.contributor:
5173 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07005174 print("Please provide contributor as 'First Last <email@example.com>'")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005175 return 1
5176
5177 base_branch = args[0]
5178
5179 if git_common.is_dirty_git_tree('land'):
5180 return 1
5181
5182 # This rev-list syntax means "show all commits not in my branch that
5183 # are in base_branch".
5184 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
5185 base_branch]).splitlines()
5186 if upstream_commits:
5187 print('Base branch "%s" has %d commits '
5188 'not in this branch.' % (base_branch, len(upstream_commits)))
5189 print('Run "git merge %s" before attempting to land.' % base_branch)
5190 return 1
5191
5192 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
5193 if not options.bypass_hooks:
5194 author = None
5195 if options.contributor:
5196 author = re.search(r'\<(.*)\>', options.contributor).group(1)
5197 hook_results = cl.RunHook(
5198 committing=True,
5199 may_prompt=not options.force,
5200 verbose=options.verbose,
5201 change=cl.GetChange(merge_base, author))
5202 if not hook_results.should_continue():
5203 return 1
5204
5205 # Check the tree status if the tree status URL is set.
5206 status = GetTreeStatus()
5207 if 'closed' == status:
5208 print('The tree is closed. Please wait for it to reopen. Use '
5209 '"git cl land --bypass-hooks" to commit on a closed tree.')
5210 return 1
5211 elif 'unknown' == status:
5212 print('Unable to determine tree status. Please verify manually and '
5213 'use "git cl land --bypass-hooks" to commit on a closed tree.')
5214 return 1
5215
5216 change_desc = ChangeDescription(options.message)
5217 if not change_desc.description and cl.GetIssue():
5218 change_desc = ChangeDescription(cl.GetDescription())
5219
5220 if not change_desc.description:
5221 if not cl.GetIssue() and options.bypass_hooks:
5222 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
5223 else:
5224 print('No description set.')
5225 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
5226 return 1
5227
5228 # Keep a separate copy for the commit message, because the commit message
5229 # contains the link to the Rietveld issue, while the Rietveld message contains
5230 # the commit viewvc url.
5231 if cl.GetIssue():
Aaron Gablea1bab272017-04-11 16:38:18 -07005232 change_desc.update_reviewers(
Robert Iannucci6c98dc62017-04-18 11:38:00 -07005233 get_approving_reviewers(cl.GetIssueProperties()), [])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005234
5235 commit_desc = ChangeDescription(change_desc.description)
5236 if cl.GetIssue():
5237 # Xcode won't linkify this URL unless there is a non-whitespace character
5238 # after it. Add a period on a new line to circumvent this. Also add a space
5239 # before the period to make sure that Gitiles continues to correctly resolve
5240 # the URL.
5241 commit_desc.append_footer('Review-Url: %s .' % cl.GetIssueURL())
5242 if options.contributor:
5243 commit_desc.append_footer('Patch from %s.' % options.contributor)
5244
5245 print('Description:')
5246 print(commit_desc.description)
5247
5248 branches = [merge_base, cl.GetBranchRef()]
5249 if not options.force:
5250 print_stats(options.similarity, options.find_copies, branches)
5251
5252 # We want to squash all this branch's commits into one commit with the proper
5253 # description. We do this by doing a "reset --soft" to the base branch (which
5254 # keeps the working copy the same), then landing that.
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005255 # Delete the special branches if they exist.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005256 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
5257 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
5258 result = RunGitWithCode(showref_cmd)
5259 if result[0] == 0:
5260 RunGit(['branch', '-D', branch])
5261
5262 # We might be in a directory that's present in this branch but not in the
5263 # trunk. Move up to the top of the tree so that git commands that expect a
5264 # valid CWD won't fail after we check out the merge branch.
5265 rel_base_path = settings.GetRelativeRoot()
5266 if rel_base_path:
5267 os.chdir(rel_base_path)
5268
5269 # Stuff our change into the merge branch.
5270 # We wrap in a try...finally block so if anything goes wrong,
5271 # we clean up the branches.
5272 retcode = -1
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005273 revision = None
5274 try:
5275 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
5276 RunGit(['reset', '--soft', merge_base])
5277 if options.contributor:
5278 RunGit(
5279 [
5280 'commit', '--author', options.contributor,
5281 '-m', commit_desc.description,
5282 ])
5283 else:
5284 RunGit(['commit', '-m', commit_desc.description])
5285
5286 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
5287 mirror = settings.GetGitMirror(remote)
5288 if mirror:
5289 pushurl = mirror.url
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005290 git_numberer_enabled = _is_git_numberer_enabled(pushurl, branch)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005291 else:
5292 pushurl = remote # Usually, this is 'origin'.
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005293 git_numberer_enabled = _is_git_numberer_enabled(
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005294 RunGit(['config', 'remote.%s.url' % remote]).strip(), branch)
5295
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005296 retcode = PushToGitWithAutoRebase(
5297 pushurl, branch, commit_desc.description, git_numberer_enabled)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005298 if retcode == 0:
5299 revision = RunGit(['rev-parse', 'HEAD']).strip()
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005300 if git_numberer_enabled:
5301 change_desc = ChangeDescription(
5302 RunGit(['show', '-s', '--format=%B', 'HEAD']).strip())
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005303 except: # pylint: disable=bare-except
5304 if _IS_BEING_TESTED:
5305 logging.exception('this is likely your ACTUAL cause of test failure.\n'
5306 + '-' * 30 + '8<' + '-' * 30)
5307 logging.error('\n' + '-' * 30 + '8<' + '-' * 30 + '\n\n\n')
5308 raise
5309 finally:
5310 # And then swap back to the original branch and clean up.
5311 RunGit(['checkout', '-q', cl.GetBranch()])
5312 RunGit(['branch', '-D', MERGE_BRANCH])
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005313 RunGit(['branch', '-D', CHERRY_PICK_BRANCH], error_ok=True)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005314
5315 if not revision:
5316 print('Failed to push. If this persists, please file a bug.')
5317 return 1
5318
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005319 if cl.GetIssue():
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005320 viewvc_url = settings.GetViewVCUrl()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005321 if viewvc_url and revision:
5322 change_desc.append_footer(
5323 'Committed: %s%s' % (viewvc_url, revision))
5324 elif revision:
5325 change_desc.append_footer('Committed: %s' % (revision,))
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005326 print('Closing issue '
5327 '(you may be prompted for your codereview password)...')
5328 cl.UpdateDescription(change_desc.description)
5329 cl.CloseIssue()
5330 props = cl.GetIssueProperties()
5331 patch_num = len(props['patchsets'])
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005332 comment = "Committed patchset #%d (id:%d) manually as %s" % (
5333 patch_num, props['patchsets'][-1], revision)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005334 if options.bypass_hooks:
5335 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
5336 else:
5337 comment += ' (presubmit successful).'
5338 cl.RpcServer().add_comment(cl.GetIssue(), comment)
5339
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005340 if os.path.isfile(POSTUPSTREAM_HOOK):
5341 RunCommand([POSTUPSTREAM_HOOK, merge_base], error_ok=True)
5342
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005343 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005344
5345
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005346def PushToGitWithAutoRebase(remote, branch, original_description,
5347 git_numberer_enabled, max_attempts=3):
5348 """Pushes current HEAD commit on top of remote's branch.
5349
5350 Attempts to fetch and autorebase on push failures.
5351 Adds git number footers on the fly.
5352
5353 Returns integer code from last command.
5354 """
5355 cherry = RunGit(['rev-parse', 'HEAD']).strip()
5356 code = 0
5357 attempts_left = max_attempts
5358 while attempts_left:
5359 attempts_left -= 1
5360 print('Attempt %d of %d' % (max_attempts - attempts_left, max_attempts))
5361
5362 # Fetch remote/branch into local cherry_pick_branch, overriding the latter.
5363 # If fetch fails, retry.
5364 print('Fetching %s/%s...' % (remote, branch))
5365 code, out = RunGitWithCode(
5366 ['retry', 'fetch', remote,
5367 '+%s:refs/heads/%s' % (branch, CHERRY_PICK_BRANCH)])
5368 if code:
5369 print('Fetch failed with exit code %d.' % code)
5370 print(out.strip())
5371 continue
5372
5373 print('Cherry-picking commit on top of latest %s' % branch)
5374 RunGitWithCode(['checkout', 'refs/heads/%s' % CHERRY_PICK_BRANCH],
5375 suppress_stderr=True)
5376 parent_hash = RunGit(['rev-parse', 'HEAD']).strip()
5377 code, out = RunGitWithCode(['cherry-pick', cherry])
5378 if code:
5379 print('Your patch doesn\'t apply cleanly to \'%s\' HEAD @ %s, '
5380 'the following files have merge conflicts:' %
5381 (branch, parent_hash))
Aaron Gable7817f022017-12-12 09:43:17 -08005382 print(RunGit(['-c', 'core.quotePath=false', 'diff',
5383 '--name-status', '--diff-filter=U']).strip())
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005384 print('Please rebase your patch and try again.')
5385 RunGitWithCode(['cherry-pick', '--abort'])
5386 break
5387
5388 commit_desc = ChangeDescription(original_description)
5389 if git_numberer_enabled:
5390 logging.debug('Adding git number footers')
5391 parent_msg = RunGit(['show', '-s', '--format=%B', parent_hash]).strip()
5392 commit_desc.update_with_git_number_footers(parent_hash, parent_msg,
5393 branch)
5394 # Ensure timestamps are monotonically increasing.
5395 timestamp = max(1 + _get_committer_timestamp(parent_hash),
5396 _get_committer_timestamp('HEAD'))
5397 _git_amend_head(commit_desc.description, timestamp)
5398
5399 code, out = RunGitWithCode(
5400 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
5401 print(out)
5402 if code == 0:
5403 break
5404 if IsFatalPushFailure(out):
5405 print('Fatal push error. Make sure your .netrc credentials and git '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02005406 'user.email are correct and you have push access to the repo.\n'
5407 'Hint: run command below to diangose common Git/Gerrit credential '
5408 'problems:\n'
5409 ' git cl creds-check\n')
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005410 break
5411 return code
5412
5413
5414def IsFatalPushFailure(push_stdout):
5415 """True if retrying push won't help."""
5416 return '(prohibited by Gerrit)' in push_stdout
5417
5418
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005419@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005420def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00005421 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005422 parser.add_option('-b', dest='newbranch',
5423 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005424 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07005425 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005426 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07005427 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005428 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005429 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00005430 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005431 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005432 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005433 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005434
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005435
5436 group = optparse.OptionGroup(
5437 parser,
5438 'Options for continuing work on the current issue uploaded from a '
5439 'different clone (e.g. different machine). Must be used independently '
5440 'from the other options. No issue number should be specified, and the '
5441 'branch must have an issue number associated with it')
5442 group.add_option('--reapply', action='store_true', dest='reapply',
5443 help='Reset the branch and reapply the issue.\n'
5444 'CAUTION: This will undo any local changes in this '
5445 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005446
5447 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005448 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005449 parser.add_option_group(group)
5450
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005451 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005452 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005453 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005454 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005455 auth_config = auth.extract_auth_config_from_options(options)
5456
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005457 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005458 if options.newbranch:
5459 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005460 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005461 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005462
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005463 cl = Changelist(auth_config=auth_config,
5464 codereview=options.forced_codereview)
5465 if not cl.GetIssue():
5466 parser.error('current branch must have an associated issue')
5467
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005468 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005469 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005470 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005471
5472 RunGit(['reset', '--hard', upstream])
5473 if options.pull:
5474 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005475
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005476 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
5477 options.directory)
5478
5479 if len(args) != 1 or not args[0]:
5480 parser.error('Must specify issue number or url')
5481
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005482 target_issue_arg = ParseIssueNumberArgument(args[0],
5483 options.forced_codereview)
5484 if not target_issue_arg.valid:
5485 parser.error('invalid codereview url or CL id')
5486
5487 cl_kwargs = {
5488 'auth_config': auth_config,
5489 'codereview_host': target_issue_arg.hostname,
5490 'codereview': options.forced_codereview,
5491 }
5492 detected_codereview_from_url = False
5493 if target_issue_arg.codereview and not options.forced_codereview:
5494 detected_codereview_from_url = True
5495 cl_kwargs['codereview'] = target_issue_arg.codereview
5496 cl_kwargs['issue'] = target_issue_arg.issue
5497
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005498 # We don't want uncommitted changes mixed up with the patch.
5499 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005500 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005501
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005502 if options.newbranch:
5503 if options.force:
5504 RunGit(['branch', '-D', options.newbranch],
5505 stderr=subprocess2.PIPE, error_ok=True)
5506 RunGit(['new-branch', options.newbranch])
5507
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005508 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005509
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005510 if cl.IsGerrit():
5511 if options.reject:
5512 parser.error('--reject is not supported with Gerrit codereview.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005513 if options.directory:
5514 parser.error('--directory is not supported with Gerrit codereview.')
5515
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005516 if detected_codereview_from_url:
5517 print('canonical issue/change URL: %s (type: %s)\n' %
5518 (cl.GetIssueURL(), target_issue_arg.codereview))
5519
5520 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07005521 options.nocommit, options.directory,
5522 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005523
5524
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005525def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005526 """Fetches the tree status and returns either 'open', 'closed',
5527 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005528 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005529 if url:
5530 status = urllib2.urlopen(url).read().lower()
5531 if status.find('closed') != -1 or status == '0':
5532 return 'closed'
5533 elif status.find('open') != -1 or status == '1':
5534 return 'open'
5535 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005536 return 'unset'
5537
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005538
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005539def GetTreeStatusReason():
5540 """Fetches the tree status from a json url and returns the message
5541 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005542 url = settings.GetTreeStatusUrl()
5543 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005544 connection = urllib2.urlopen(json_url)
5545 status = json.loads(connection.read())
5546 connection.close()
5547 return status['message']
5548
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005549
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005550def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005551 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005552 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005553 status = GetTreeStatus()
5554 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005555 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005556 return 2
5557
vapiera7fbd5a2016-06-16 09:17:49 -07005558 print('The tree is %s' % status)
5559 print()
5560 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005561 if status != 'open':
5562 return 1
5563 return 0
5564
5565
maruel@chromium.org15192402012-09-06 12:38:29 +00005566def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005567 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005568 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005569 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005570 '-b', '--bot', action='append',
5571 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5572 'times to specify multiple builders. ex: '
5573 '"-b win_rel -b win_layout". See '
5574 'the try server waterfall for the builders name and the tests '
5575 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005576 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005577 '-B', '--bucket', default='',
5578 help=('Buildbucket bucket to send the try requests.'))
5579 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005580 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005581 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005582 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005583 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005584 help='Revision to use for the try job; default: the revision will '
5585 'be determined by the try recipe that builder runs, which usually '
5586 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005587 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005588 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005589 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005590 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005591 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005592 '--project',
5593 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005594 'in recipe to determine to which repository or directory to '
5595 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005596 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005597 '-p', '--property', dest='properties', action='append', default=[],
5598 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005599 'key2=value2 etc. The value will be treated as '
5600 'json if decodable, or as string otherwise. '
5601 'NOTE: using this may make your try job not usable for CQ, '
5602 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005603 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005604 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5605 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005606 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005607 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09005608 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005609 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09005610 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005611 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005612
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005613 if options.master and options.master.startswith('luci.'):
5614 parser.error(
5615 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005616 # Make sure that all properties are prop=value pairs.
5617 bad_params = [x for x in options.properties if '=' not in x]
5618 if bad_params:
5619 parser.error('Got properties with missing "=": %s' % bad_params)
5620
maruel@chromium.org15192402012-09-06 12:38:29 +00005621 if args:
5622 parser.error('Unknown arguments: %s' % args)
5623
Koji Ishii31c14782018-01-08 17:17:33 +09005624 cl = Changelist(auth_config=auth_config, issue=options.issue,
5625 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00005626 if not cl.GetIssue():
5627 parser.error('Need to upload first')
5628
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005629 if cl.IsGerrit():
5630 # HACK: warm up Gerrit change detail cache to save on RPCs.
5631 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5632
tandriie113dfd2016-10-11 10:20:12 -07005633 error_message = cl.CannotTriggerTryJobReason()
5634 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005635 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005636
borenet6c0efe62016-10-19 08:13:29 -07005637 if options.bucket and options.master:
5638 parser.error('Only one of --bucket and --master may be used.')
5639
qyearsley1fdfcb62016-10-24 13:22:03 -07005640 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005641
qyearsleydd49f942016-10-28 11:57:22 -07005642 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5643 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005644 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005645 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005646 print('git cl try with no bots now defaults to CQ dry run.')
5647 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5648 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005649
borenet6c0efe62016-10-19 08:13:29 -07005650 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005651 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005652 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005653 'of bot requires an initial job from a parent (usually a builder). '
5654 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005655 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005656 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005657
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005658 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005659 # TODO(tandrii): Checking local patchset against remote patchset is only
5660 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5661 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07005662 print('Warning: Codereview server has newer patchsets (%s) than most '
5663 'recent upload from local checkout (%s). Did a previous upload '
5664 'fail?\n'
5665 'By default, git cl try uses the latest patchset from '
5666 'codereview, continuing to use patchset %s.\n' %
5667 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07005668
tandrii568043b2016-10-11 07:49:18 -07005669 try:
borenet6c0efe62016-10-19 08:13:29 -07005670 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
5671 patchset)
tandrii568043b2016-10-11 07:49:18 -07005672 except BuildbucketResponseException as ex:
5673 print('ERROR: %s' % ex)
5674 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005675 return 0
5676
5677
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005678def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005679 """Prints info about try jobs associated with current CL."""
5680 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005681 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005682 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005683 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005684 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005685 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005686 '--color', action='store_true', default=setup_color.IS_TTY,
5687 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005688 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005689 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5690 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005691 group.add_option(
Stefan Zager1306bd02017-06-22 19:26:46 -07005692 '--json', help=('Path of JSON output file to write try job results to,'
5693 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005694 parser.add_option_group(group)
5695 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07005696 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005697 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07005698 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005699 if args:
5700 parser.error('Unrecognized args: %s' % ' '.join(args))
5701
5702 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07005703 cl = Changelist(
5704 issue=options.issue, codereview=options.forced_codereview,
5705 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005706 if not cl.GetIssue():
5707 parser.error('Need to upload first')
5708
tandrii221ab252016-10-06 08:12:04 -07005709 patchset = options.patchset
5710 if not patchset:
5711 patchset = cl.GetMostRecentPatchset()
5712 if not patchset:
5713 parser.error('Codereview doesn\'t know about issue %s. '
5714 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005715 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005716 cl.GetIssue())
5717
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005718 # TODO(tandrii): Checking local patchset against remote patchset is only
5719 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5720 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07005721 print('Warning: Codereview server has newer patchsets (%s) than most '
5722 'recent upload from local checkout (%s). Did a previous upload '
5723 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07005724 'By default, git cl try-results uses the latest patchset from '
5725 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07005726 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005727 try:
tandrii221ab252016-10-06 08:12:04 -07005728 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005729 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005730 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005731 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005732 if options.json:
5733 write_try_results_json(options.json, jobs)
5734 else:
5735 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005736 return 0
5737
5738
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005739@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005740def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005741 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005742 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005743 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005744 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005745
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005746 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005747 if args:
5748 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005749 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005750 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005751 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005752 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005753
5754 # Clear configured merge-base, if there is one.
5755 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005756 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005757 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005758 return 0
5759
5760
thestig@chromium.org00858c82013-12-02 23:08:03 +00005761def CMDweb(parser, args):
5762 """Opens the current CL in the web browser."""
5763 _, args = parser.parse_args(args)
5764 if args:
5765 parser.error('Unrecognized args: %s' % ' '.join(args))
5766
5767 issue_url = Changelist().GetIssueURL()
5768 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005769 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005770 return 1
5771
5772 webbrowser.open(issue_url)
5773 return 0
5774
5775
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005776def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005777 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005778 parser.add_option('-d', '--dry-run', action='store_true',
5779 help='trigger in dry run mode')
5780 parser.add_option('-c', '--clear', action='store_true',
5781 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005782 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005783 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005784 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005785 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005786 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005787 if args:
5788 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005789 if options.dry_run and options.clear:
5790 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5791
iannuccie53c9352016-08-17 14:40:40 -07005792 cl = Changelist(auth_config=auth_config, issue=options.issue,
5793 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005794 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005795 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005796 elif options.dry_run:
5797 state = _CQState.DRY_RUN
5798 else:
5799 state = _CQState.COMMIT
5800 if not cl.GetIssue():
5801 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005802 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005803 return 0
5804
5805
groby@chromium.org411034a2013-02-26 15:12:01 +00005806def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005807 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005808 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005809 auth.add_auth_options(parser)
5810 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005811 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005812 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005813 if args:
5814 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005815 cl = Changelist(auth_config=auth_config, issue=options.issue,
5816 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005817 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005818 if not cl.GetIssue():
5819 DieWithError('ERROR No issue to close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005820 cl.CloseIssue()
5821 return 0
5822
5823
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005824def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005825 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005826 parser.add_option(
5827 '--stat',
5828 action='store_true',
5829 dest='stat',
5830 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005831 auth.add_auth_options(parser)
5832 options, args = parser.parse_args(args)
5833 auth_config = auth.extract_auth_config_from_options(options)
5834 if args:
5835 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005836
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005837 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005838 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005839 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005840 if not issue:
5841 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005842
Aaron Gablea718c3e2017-08-28 17:47:28 -07005843 base = cl._GitGetBranchConfigValue('last-upload-hash')
5844 if not base:
5845 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5846 if not base:
5847 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5848 revision_info = detail['revisions'][detail['current_revision']]
5849 fetch_info = revision_info['fetch']['http']
5850 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5851 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005852
Aaron Gablea718c3e2017-08-28 17:47:28 -07005853 cmd = ['git', 'diff']
5854 if options.stat:
5855 cmd.append('--stat')
5856 cmd.append(base)
5857 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005858
5859 return 0
5860
5861
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005862def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005863 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005864 parser.add_option(
5865 '--no-color',
5866 action='store_true',
5867 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005868 parser.add_option(
5869 '--batch',
5870 action='store_true',
5871 help='Do not run interactively, just suggest some')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005872 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005873 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005874 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005875
5876 author = RunGit(['config', 'user.email']).strip() or None
5877
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005878 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005879
5880 if args:
5881 if len(args) > 1:
5882 parser.error('Unknown args')
5883 base_branch = args[0]
5884 else:
5885 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005886 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005887
5888 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005889 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5890
5891 if options.batch:
5892 db = owners.Database(change.RepositoryRoot(), file, os.path)
5893 print('\n'.join(db.reviewers_for(affected_files, author)))
5894 return 0
5895
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005896 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005897 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005898 change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02005899 author, fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005900 disable_color=options.no_color,
5901 override_files=change.OriginalOwnersFiles()).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005902
5903
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005904def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005905 """Generates a diff command."""
5906 # Generate diff for the current branch's changes.
Aaron Gablef4068aa2017-12-12 15:14:09 -08005907 diff_cmd = ['-c', 'core.quotePath=false', 'diff',
5908 '--no-ext-diff', '--no-prefix', diff_type,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005909 upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005910
5911 if args:
5912 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005913 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005914 diff_cmd.append(arg)
5915 else:
5916 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005917
5918 return diff_cmd
5919
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005920
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005921def MatchingFileType(file_name, extensions):
5922 """Returns true if the file name ends with one of the given extensions."""
5923 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005924
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005925
enne@chromium.org555cfe42014-01-29 18:21:39 +00005926@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005927def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005928 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005929 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005930 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005931 parser.add_option('--full', action='store_true',
5932 help='Reformat the full content of all touched files')
5933 parser.add_option('--dry-run', action='store_true',
5934 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005935 parser.add_option('--python', action='store_true',
5936 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005937 parser.add_option('--js', action='store_true',
5938 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005939 parser.add_option('--diff', action='store_true',
5940 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005941 parser.add_option('--presubmit', action='store_true',
5942 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005943 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005944
Daniel Chengc55eecf2016-12-30 03:11:02 -08005945 # Normalize any remaining args against the current path, so paths relative to
5946 # the current directory are still resolved as expected.
5947 args = [os.path.join(os.getcwd(), arg) for arg in args]
5948
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005949 # git diff generates paths against the root of the repository. Change
5950 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005951 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005952 if rel_base_path:
5953 os.chdir(rel_base_path)
5954
digit@chromium.org29e47272013-05-17 17:01:46 +00005955 # Grab the merge-base commit, i.e. the upstream commit of the current
5956 # branch when it was created or the last time it was rebased. This is
5957 # to cover the case where the user may have called "git fetch origin",
5958 # moving the origin branch to a newer commit, but hasn't rebased yet.
5959 upstream_commit = None
5960 cl = Changelist()
5961 upstream_branch = cl.GetUpstreamBranch()
5962 if upstream_branch:
5963 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5964 upstream_commit = upstream_commit.strip()
5965
5966 if not upstream_commit:
5967 DieWithError('Could not find base commit for this branch. '
5968 'Are you in detached state?')
5969
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005970 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5971 diff_output = RunGit(changed_files_cmd)
5972 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005973 # Filter out files deleted by this CL
5974 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005975
Christopher Lamc5ba6922017-01-24 11:19:14 +11005976 if opts.js:
5977 CLANG_EXTS.append('.js')
5978
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005979 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5980 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5981 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005982 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005983
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005984 top_dir = os.path.normpath(
5985 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5986
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005987 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5988 # formatted. This is used to block during the presubmit.
5989 return_value = 0
5990
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005991 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005992 # Locate the clang-format binary in the checkout
5993 try:
5994 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005995 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005996 DieWithError(e)
5997
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005998 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005999 cmd = [clang_format_tool]
6000 if not opts.dry_run and not opts.diff:
6001 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006002 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006003 if opts.diff:
6004 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006005 else:
6006 env = os.environ.copy()
6007 env['PATH'] = str(os.path.dirname(clang_format_tool))
6008 try:
6009 script = clang_format.FindClangFormatScriptInChromiumTree(
6010 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07006011 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006012 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00006013
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006014 cmd = [sys.executable, script, '-p0']
6015 if not opts.dry_run and not opts.diff:
6016 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00006017
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006018 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
6019 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006020
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006021 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
6022 if opts.diff:
6023 sys.stdout.write(stdout)
6024 if opts.dry_run and len(stdout) > 0:
6025 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006026
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006027 # Similar code to above, but using yapf on .py files rather than clang-format
6028 # on C/C++ files
6029 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006030 yapf_tool = gclient_utils.FindExecutable('yapf')
6031 if yapf_tool is None:
6032 DieWithError('yapf not found in PATH')
6033
6034 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006035 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006036 cmd = [yapf_tool]
6037 if not opts.dry_run and not opts.diff:
6038 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006039 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006040 if opts.diff:
6041 sys.stdout.write(stdout)
6042 else:
6043 # TODO(sbc): yapf --lines mode still has some issues.
6044 # https://github.com/google/yapf/issues/154
6045 DieWithError('--python currently only works with --full')
6046
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006047 # Dart's formatter does not have the nice property of only operating on
6048 # modified chunks, so hard code full.
6049 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006050 try:
6051 command = [dart_format.FindDartFmtToolInChromiumTree()]
6052 if not opts.dry_run and not opts.diff:
6053 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006054 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006055
ppi@chromium.org6593d932016-03-03 15:41:15 +00006056 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006057 if opts.dry_run and stdout:
6058 return_value = 2
6059 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07006060 print('Warning: Unable to check Dart code formatting. Dart SDK not '
6061 'found in this checkout. Files in other languages are still '
6062 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006063
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006064 # Format GN build files. Always run on full build files for canonical form.
6065 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01006066 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07006067 if opts.dry_run or opts.diff:
6068 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006069 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07006070 gn_ret = subprocess2.call(cmd + [gn_diff_file],
6071 shell=sys.platform == 'win32',
6072 cwd=top_dir)
6073 if opts.dry_run and gn_ret == 2:
6074 return_value = 2 # Not formatted.
6075 elif opts.diff and gn_ret == 2:
6076 # TODO this should compute and print the actual diff.
6077 print("This change has GN build file diff for " + gn_diff_file)
6078 elif gn_ret != 0:
6079 # For non-dry run cases (and non-2 return values for dry-run), a
6080 # nonzero error code indicates a failure, probably because the file
6081 # doesn't parse.
6082 DieWithError("gn format failed on " + gn_diff_file +
6083 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006084
Ilya Shermane081cbe2017-08-15 17:51:04 -07006085 # Skip the metrics formatting from the global presubmit hook. These files have
6086 # a separate presubmit hook that issues an error if the files need formatting,
6087 # whereas the top-level presubmit script merely issues a warning. Formatting
6088 # these files is somewhat slow, so it's important not to duplicate the work.
6089 if not opts.presubmit:
6090 for xml_dir in GetDirtyMetricsDirs(diff_files):
6091 tool_dir = os.path.join(top_dir, xml_dir)
6092 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
6093 if opts.dry_run or opts.diff:
6094 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07006095 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07006096 if opts.diff:
6097 sys.stdout.write(stdout)
6098 if opts.dry_run and stdout:
6099 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05006100
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006101 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006102
Steven Holte2e664bf2017-04-21 13:10:47 -07006103def GetDirtyMetricsDirs(diff_files):
6104 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
6105 metrics_xml_dirs = [
6106 os.path.join('tools', 'metrics', 'actions'),
6107 os.path.join('tools', 'metrics', 'histograms'),
6108 os.path.join('tools', 'metrics', 'rappor'),
6109 os.path.join('tools', 'metrics', 'ukm')]
6110 for xml_dir in metrics_xml_dirs:
6111 if any(file.startswith(xml_dir) for file in xml_diff_files):
6112 yield xml_dir
6113
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006114
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006115@subcommand.usage('<codereview url or issue id>')
6116def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006117 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006118 _, args = parser.parse_args(args)
6119
6120 if len(args) != 1:
6121 parser.print_help()
6122 return 1
6123
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00006124 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00006125 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02006126 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02006127
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00006128 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006129
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006130 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00006131 output = RunGit(['config', '--local', '--get-regexp',
6132 r'branch\..*\.%s' % issueprefix],
6133 error_ok=True)
6134 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006135 if issue == target_issue:
6136 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006137
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006138 branches = []
6139 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07006140 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006141 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07006142 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006143 return 1
6144 if len(branches) == 1:
6145 RunGit(['checkout', branches[0]])
6146 else:
vapiera7fbd5a2016-06-16 09:17:49 -07006147 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006148 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07006149 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006150 which = raw_input('Choose by index: ')
6151 try:
6152 RunGit(['checkout', branches[int(which)]])
6153 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07006154 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006155 return 1
6156
6157 return 0
6158
6159
maruel@chromium.org29404b52014-09-08 22:58:00 +00006160def CMDlol(parser, args):
6161 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07006162 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00006163 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
6164 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
6165 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07006166 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00006167 return 0
6168
6169
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006170class OptionParser(optparse.OptionParser):
6171 """Creates the option parse and add --verbose support."""
6172 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006173 optparse.OptionParser.__init__(
6174 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006175 self.add_option(
6176 '-v', '--verbose', action='count', default=0,
6177 help='Use 2 times for more debugging info')
6178
6179 def parse_args(self, args=None, values=None):
6180 options, args = optparse.OptionParser.parse_args(self, args, values)
6181 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01006182 logging.basicConfig(
6183 level=levels[min(options.verbose, len(levels) - 1)],
6184 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
6185 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006186 return options, args
6187
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006188
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006189def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006190 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07006191 print('\nYour python version %s is unsupported, please upgrade.\n' %
6192 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006193 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006194
maruel@chromium.orgddd59412011-11-30 14:20:38 +00006195 # Reload settings.
6196 global settings
6197 settings = Settings()
6198
maruel@chromium.org39c0b222013-08-17 16:57:01 +00006199 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006200 dispatcher = subcommand.CommandDispatcher(__name__)
6201 try:
6202 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00006203 except auth.AuthenticationError as e:
6204 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07006205 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006206 if e.code != 500:
6207 raise
6208 DieWithError(
6209 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
6210 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00006211 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006212
6213
6214if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006215 # These affect sys.stdout so do it outside of main() to simplify mocks in
6216 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00006217 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00006218 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00006219 try:
6220 sys.exit(main(sys.argv[1:]))
6221 except KeyboardInterrupt:
6222 sys.stderr.write('interrupted\n')
6223 sys.exit(1)