blob: 5a9db7eda1182254bd6a0f0756c0c5e59b30baa2 [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
Edward Lemur707d70b2018-02-07 00:50:14 +01001753 def GetReviewers(self):
1754 return self._codereview_impl.GetReviewers()
1755
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001756 def GetMostRecentPatchset(self):
1757 return self._codereview_impl.GetMostRecentPatchset()
1758
tandriide281ae2016-10-12 06:02:30 -07001759 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001760 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriide281ae2016-10-12 06:02:30 -07001761 return self._codereview_impl.CannotTriggerTryJobReason()
1762
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001763 def GetTryJobProperties(self, patchset=None):
1764 """Returns dictionary of properties to launch try job."""
1765 return self._codereview_impl.GetTryJobProperties(patchset=patchset)
tandrii8c5a3532016-11-04 07:52:02 -07001766
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001767 def __getattr__(self, attr):
1768 # This is because lots of untested code accesses Rietveld-specific stuff
1769 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001770 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001771 # Note that child method defines __getattr__ as well, and forwards it here,
1772 # because _RietveldChangelistImpl is not cleaned up yet, and given
1773 # deprecation of Rietveld, it should probably be just removed.
1774 # Until that time, avoid infinite recursion by bypassing __getattr__
1775 # of implementation class.
1776 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001777
1778
1779class _ChangelistCodereviewBase(object):
1780 """Abstract base class encapsulating codereview specifics of a changelist."""
1781 def __init__(self, changelist):
1782 self._changelist = changelist # instance of Changelist
1783
1784 def __getattr__(self, attr):
1785 # Forward methods to changelist.
1786 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1787 # _RietveldChangelistImpl to avoid this hack?
1788 return getattr(self._changelist, attr)
1789
1790 def GetStatus(self):
1791 """Apply a rough heuristic to give a simple summary of an issue's review
1792 or CQ status, assuming adherence to a common workflow.
1793
1794 Returns None if no issue for this branch, or specific string keywords.
1795 """
1796 raise NotImplementedError()
1797
1798 def GetCodereviewServer(self):
1799 """Returns server URL without end slash, like "https://codereview.com"."""
1800 raise NotImplementedError()
1801
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001802 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001803 """Fetches and returns description from the codereview server."""
1804 raise NotImplementedError()
1805
tandrii5d48c322016-08-18 16:19:37 -07001806 @classmethod
1807 def IssueConfigKey(cls):
1808 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001809 raise NotImplementedError()
1810
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001811 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001812 def PatchsetConfigKey(cls):
1813 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001814 raise NotImplementedError()
1815
tandrii5d48c322016-08-18 16:19:37 -07001816 @classmethod
1817 def CodereviewServerConfigKey(cls):
1818 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001819 raise NotImplementedError()
1820
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001821 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001822 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001823 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001824
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001825 def GetRietveldObjForPresubmit(self):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001826 # This is an unfortunate Rietveld-embeddedness in presubmit.
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001827 # For non-Rietveld code reviews, this probably should return a dummy object.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001828 raise NotImplementedError()
1829
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001830 def GetGerritObjForPresubmit(self):
1831 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1832 return None
1833
dsansomee2d6fd92016-09-08 00:10:47 -07001834 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001835 """Update the description on codereview site."""
1836 raise NotImplementedError()
1837
Aaron Gable636b13f2017-07-14 10:42:48 -07001838 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001839 """Posts a comment to the codereview site."""
1840 raise NotImplementedError()
1841
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001842 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001843 raise NotImplementedError()
1844
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001845 def CloseIssue(self):
1846 """Closes the issue."""
1847 raise NotImplementedError()
1848
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001849 def GetMostRecentPatchset(self):
1850 """Returns the most recent patchset number from the codereview site."""
1851 raise NotImplementedError()
1852
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001853 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07001854 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001855 """Fetches and applies the issue.
1856
1857 Arguments:
1858 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1859 reject: if True, reject the failed patch instead of switching to 3-way
1860 merge. Rietveld only.
1861 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1862 only.
1863 directory: switch to directory before applying the patch. Rietveld only.
Aaron Gable62619a32017-06-16 08:22:09 -07001864 force: if true, overwrites existing local state.
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001865 """
1866 raise NotImplementedError()
1867
1868 @staticmethod
1869 def ParseIssueURL(parsed_url):
1870 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1871 failed."""
1872 raise NotImplementedError()
1873
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001874 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001875 """Best effort check that user is authenticated with codereview server.
1876
1877 Arguments:
1878 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001879 refresh: whether to attempt to refresh credentials. Ignored if not
1880 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001881 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001882 raise NotImplementedError()
1883
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001884 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001885 """Best effort check that uploading isn't supposed to fail for predictable
1886 reasons.
1887
1888 This method should raise informative exception if uploading shouldn't
1889 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001890
1891 Arguments:
1892 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001893 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001894 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001895
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001896 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001897 """Uploads a change to codereview."""
1898 raise NotImplementedError()
1899
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001900 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001901 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001902
1903 Issue must have been already uploaded and known.
1904 """
1905 raise NotImplementedError()
1906
tandriie113dfd2016-10-11 10:20:12 -07001907 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001908 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriie113dfd2016-10-11 10:20:12 -07001909 raise NotImplementedError()
1910
tandriide281ae2016-10-12 06:02:30 -07001911 def GetIssueOwner(self):
1912 raise NotImplementedError()
1913
Edward Lemur707d70b2018-02-07 00:50:14 +01001914 def GetReviewers(self):
1915 raise NotImplementedError()
1916
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001917 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001918 raise NotImplementedError()
1919
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001920
1921class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001922
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001923 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001924 super(_RietveldChangelistImpl, self).__init__(changelist)
1925 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001926 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001927 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001928
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001929 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001930 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001931 self._props = None
1932 self._rpc_server = None
1933
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001934 def GetCodereviewServer(self):
1935 if not self._rietveld_server:
1936 # If we're on a branch then get the server potentially associated
1937 # with that branch.
1938 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001939 self._rietveld_server = gclient_utils.UpgradeToHttps(
1940 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001941 if not self._rietveld_server:
1942 self._rietveld_server = settings.GetDefaultServerUrl()
1943 return self._rietveld_server
1944
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001945 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001946 """Best effort check that user is authenticated with Rietveld server."""
1947 if self._auth_config.use_oauth2:
1948 authenticator = auth.get_authenticator_for_host(
1949 self.GetCodereviewServer(), self._auth_config)
1950 if not authenticator.has_cached_credentials():
1951 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001952 if refresh:
1953 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001954
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001955 def EnsureCanUploadPatchset(self, force):
1956 # No checks for Rietveld because we are deprecating Rietveld.
1957 pass
1958
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001959 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001960 issue = self.GetIssue()
1961 assert issue
1962 try:
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001963 return self.RpcServer().get_description(issue, force=force).strip()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001964 except urllib2.HTTPError as e:
1965 if e.code == 404:
1966 DieWithError(
1967 ('\nWhile fetching the description for issue %d, received a '
1968 '404 (not found)\n'
1969 'error. It is likely that you deleted this '
1970 'issue on the server. If this is the\n'
1971 'case, please run\n\n'
1972 ' git cl issue 0\n\n'
1973 'to clear the association with the deleted issue. Then run '
1974 'this command again.') % issue)
1975 else:
1976 DieWithError(
1977 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1978 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001979 print('Warning: Failed to retrieve CL description due to network '
1980 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001981 return ''
1982
1983 def GetMostRecentPatchset(self):
1984 return self.GetIssueProperties()['patchsets'][-1]
1985
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001986 def GetIssueProperties(self):
1987 if self._props is None:
1988 issue = self.GetIssue()
1989 if not issue:
1990 self._props = {}
1991 else:
1992 self._props = self.RpcServer().get_issue_properties(issue, True)
1993 return self._props
1994
tandriie113dfd2016-10-11 10:20:12 -07001995 def CannotTriggerTryJobReason(self):
1996 props = self.GetIssueProperties()
1997 if not props:
1998 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1999 if props.get('closed'):
2000 return 'CL %s is closed' % self.GetIssue()
2001 if props.get('private'):
2002 return 'CL %s is private' % self.GetIssue()
2003 return None
2004
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002005 def GetTryJobProperties(self, patchset=None):
2006 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07002007 project = (self.GetIssueProperties() or {}).get('project')
2008 return {
2009 'issue': self.GetIssue(),
2010 'patch_project': project,
2011 'patch_storage': 'rietveld',
2012 'patchset': patchset or self.GetPatchset(),
2013 'rietveld': self.GetCodereviewServer(),
2014 }
2015
tandriide281ae2016-10-12 06:02:30 -07002016 def GetIssueOwner(self):
2017 return (self.GetIssueProperties() or {}).get('owner_email')
2018
Edward Lemur707d70b2018-02-07 00:50:14 +01002019 def GetReviewers(self):
2020 return (self.GetIssueProperties() or {}).get('reviewers')
2021
Aaron Gable636b13f2017-07-14 10:42:48 -07002022 def AddComment(self, message, publish=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002023 return self.RpcServer().add_comment(self.GetIssue(), message)
2024
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002025 def GetCommentsSummary(self, _readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002026 summary = []
2027 for message in self.GetIssueProperties().get('messages', []):
2028 date = datetime.datetime.strptime(message['date'], '%Y-%m-%d %H:%M:%S.%f')
2029 summary.append(_CommentSummary(
2030 date=date,
2031 disapproval=bool(message['disapproval']),
2032 approval=bool(message['approval']),
2033 sender=message['sender'],
2034 message=message['text'],
2035 ))
2036 return summary
2037
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002038 def GetStatus(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002039 """Applies a rough heuristic to give a simple summary of an issue's review
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002040 or CQ status, assuming adherence to a common workflow.
2041
2042 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gablea1bab272017-04-11 16:38:18 -07002043 * 'error' - error from review tool (including deleted issues)
2044 * 'unsent' - not sent for review
2045 * 'waiting' - waiting for review
2046 * 'reply' - waiting for owner to reply to review
2047 * 'not lgtm' - Code-Review label has been set negatively
2048 * 'lgtm' - LGTM from at least one approved reviewer
2049 * 'commit' - in the commit queue
2050 * 'closed' - closed
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002051 """
2052 if not self.GetIssue():
2053 return None
2054
2055 try:
2056 props = self.GetIssueProperties()
2057 except urllib2.HTTPError:
2058 return 'error'
2059
2060 if props.get('closed'):
2061 # Issue is closed.
2062 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00002063 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002064 # Issue is in the commit queue.
2065 return 'commit'
2066
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002067 messages = props.get('messages') or []
Aaron Gablea1bab272017-04-11 16:38:18 -07002068 if not messages:
2069 # No message was sent.
2070 return 'unsent'
2071
2072 if get_approving_reviewers(props):
2073 return 'lgtm'
2074 elif get_approving_reviewers(props, disapproval=True):
2075 return 'not lgtm'
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002076
tandrii9d2c7a32016-06-22 03:42:45 -07002077 # Skip CQ messages that don't require owner's action.
2078 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
2079 if 'Dry run:' in messages[-1]['text']:
2080 messages.pop()
2081 elif 'The CQ bit was unchecked' in messages[-1]['text']:
2082 # This message always follows prior messages from CQ,
2083 # so skip this too.
2084 messages.pop()
2085 else:
2086 # This is probably a CQ messages warranting user attention.
2087 break
2088
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002089 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07002090 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002091 return 'reply'
2092 return 'waiting'
2093
dsansomee2d6fd92016-09-08 00:10:47 -07002094 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01002095 self.RpcServer().update_description(self.GetIssue(), description)
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002096
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002097 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002098 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002099
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002100 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002101 return self.SetFlags({flag: value})
2102
2103 def SetFlags(self, flags):
2104 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002105 """
phajdan.jr68598232016-08-10 03:28:28 -07002106 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002107 try:
tandrii4b233bd2016-07-06 03:50:29 -07002108 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002109 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002110 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002111 if e.code == 404:
2112 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2113 if e.code == 403:
2114 DieWithError(
2115 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002116 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002117 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002118
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002119 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002120 """Returns an upload.RpcServer() to access this review's rietveld instance.
2121 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002122 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002123 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002124 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01002125 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002126 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002127
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002128 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002129 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002130 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002131
tandrii5d48c322016-08-18 16:19:37 -07002132 @classmethod
2133 def PatchsetConfigKey(cls):
2134 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002135
tandrii5d48c322016-08-18 16:19:37 -07002136 @classmethod
2137 def CodereviewServerConfigKey(cls):
2138 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002139
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002140 def GetRietveldObjForPresubmit(self):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002141 return self.RpcServer()
2142
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002143 def SetCQState(self, new_state):
2144 props = self.GetIssueProperties()
2145 if props.get('private'):
2146 DieWithError('Cannot set-commit on private issue')
2147
2148 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002149 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002150 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002151 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002152 else:
tandrii4b233bd2016-07-06 03:50:29 -07002153 assert new_state == _CQState.DRY_RUN
2154 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002155
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002156 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002157 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002158 # PatchIssue should never be called with a dirty tree. It is up to the
2159 # caller to check this, but just in case we assert here since the
2160 # consequences of the caller not checking this could be dire.
2161 assert(not git_common.is_dirty_git_tree('apply'))
2162 assert(parsed_issue_arg.valid)
2163 self._changelist.issue = parsed_issue_arg.issue
2164 if parsed_issue_arg.hostname:
2165 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2166
skobes6468b902016-10-24 08:45:10 -07002167 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2168 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2169 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002170 try:
skobes6468b902016-10-24 08:45:10 -07002171 scm_obj.apply_patch(patchset_object)
2172 except Exception as e:
2173 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002174 return 1
2175
2176 # If we had an issue, commit the current state and register the issue.
2177 if not nocommit:
Aaron Gabled343c632017-03-15 11:02:26 -07002178 self.SetIssue(self.GetIssue())
2179 self.SetPatchset(patchset)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002180 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2181 'patch from issue %(i)s at patchset '
2182 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2183 % {'i': self.GetIssue(), 'p': patchset})])
vapiera7fbd5a2016-06-16 09:17:49 -07002184 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002185 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002186 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002187 return 0
2188
2189 @staticmethod
2190 def ParseIssueURL(parsed_url):
2191 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2192 return None
wychen3c1c1722016-08-04 11:46:36 -07002193 # Rietveld patch: https://domain/<number>/#ps<patchset>
2194 match = re.match(r'/(\d+)/$', parsed_url.path)
2195 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2196 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002197 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002198 issue=int(match.group(1)),
2199 patchset=int(match2.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002200 hostname=parsed_url.netloc,
2201 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002202 # Typical url: https://domain/<issue_number>[/[other]]
2203 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2204 if match:
skobes6468b902016-10-24 08:45:10 -07002205 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002206 issue=int(match.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002207 hostname=parsed_url.netloc,
2208 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002209 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2210 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2211 if match:
skobes6468b902016-10-24 08:45:10 -07002212 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002213 issue=int(match.group(1)),
2214 patchset=int(match.group(2)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002215 hostname=parsed_url.netloc,
2216 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002217 return None
2218
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002219 def CMDUploadChange(self, options, args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002220 """Upload the patch to Rietveld."""
2221 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2222 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002223 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2224 if options.emulate_svn_auto_props:
2225 upload_args.append('--emulate_svn_auto_props')
2226
2227 change_desc = None
2228
2229 if options.email is not None:
2230 upload_args.extend(['--email', options.email])
2231
2232 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002233 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002234 upload_args.extend(['--title', options.title])
2235 if options.message:
2236 upload_args.extend(['--message', options.message])
2237 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002238 print('This branch is associated with issue %s. '
2239 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002240 else:
nodirca166002016-06-27 10:59:51 -07002241 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002242 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002243 if options.message:
2244 message = options.message
2245 else:
2246 message = CreateDescriptionFromLog(args)
2247 if options.title:
2248 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002249 change_desc = ChangeDescription(message)
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002250 if options.reviewers or options.add_owners_to:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002251 change_desc.update_reviewers(options.reviewers, options.tbrs,
2252 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002253 if not options.force:
Aaron Gable3a16ed12017-03-23 10:51:55 -07002254 change_desc.prompt(bug=options.bug, git_footer=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002255
2256 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002257 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002258 return 1
2259
2260 upload_args.extend(['--message', change_desc.description])
2261 if change_desc.get_reviewers():
2262 upload_args.append('--reviewers=%s' % ','.join(
2263 change_desc.get_reviewers()))
2264 if options.send_mail:
2265 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002266 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002267 upload_args.append('--send_mail')
2268
2269 # We check this before applying rietveld.private assuming that in
2270 # rietveld.cc only addresses which we can send private CLs to are listed
2271 # if rietveld.private is set, and so we should ignore rietveld.cc only
2272 # when --private is specified explicitly on the command line.
2273 if options.private:
2274 logging.warn('rietveld.cc is ignored since private flag is specified. '
2275 'You need to review and add them manually if necessary.')
2276 cc = self.GetCCListWithoutDefault()
2277 else:
2278 cc = self.GetCCList()
2279 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002280 if change_desc.get_cced():
2281 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002282 if cc:
2283 upload_args.extend(['--cc', cc])
2284
2285 if options.private or settings.GetDefaultPrivateFlag() == "True":
2286 upload_args.append('--private')
2287
2288 upload_args.extend(['--git_similarity', str(options.similarity)])
2289 if not options.find_copies:
2290 upload_args.extend(['--git_no_find_copies'])
2291
2292 # Include the upstream repo's URL in the change -- this is useful for
2293 # projects that have their source spread across multiple repos.
2294 remote_url = self.GetGitBaseUrlFromConfig()
2295 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002296 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2297 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2298 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002299 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002300 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002301 target_ref = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002302 if target_ref:
2303 upload_args.extend(['--target_ref', target_ref])
2304
2305 # Look for dependent patchsets. See crbug.com/480453 for more details.
2306 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2307 upstream_branch = ShortBranchName(upstream_branch)
2308 if remote is '.':
2309 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002310 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002311 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002312 print()
2313 print('Skipping dependency patchset upload because git config '
2314 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2315 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002316 else:
2317 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002318 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002319 auth_config=auth_config)
2320 branch_cl_issue_url = branch_cl.GetIssueURL()
2321 branch_cl_issue = branch_cl.GetIssue()
2322 branch_cl_patchset = branch_cl.GetPatchset()
2323 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2324 upload_args.extend(
2325 ['--depends_on_patchset', '%s:%s' % (
2326 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002327 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002328 '\n'
2329 'The current branch (%s) is tracking a local branch (%s) with '
2330 'an associated CL.\n'
2331 'Adding %s/#ps%s as a dependency patchset.\n'
2332 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2333 branch_cl_patchset))
2334
2335 project = settings.GetProject()
2336 if project:
2337 upload_args.extend(['--project', project])
Aaron Gable665a4392017-06-29 10:53:46 -07002338 else:
2339 print()
2340 print('WARNING: Uploading without a project specified. Please ensure '
2341 'your repo\'s codereview.settings has a "PROJECT: foo" line.')
2342 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002343
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002344 try:
2345 upload_args = ['upload'] + upload_args + args
2346 logging.info('upload.RealMain(%s)', upload_args)
2347 issue, patchset = upload.RealMain(upload_args)
2348 issue = int(issue)
2349 patchset = int(patchset)
2350 except KeyboardInterrupt:
2351 sys.exit(1)
2352 except:
2353 # If we got an exception after the user typed a description for their
2354 # change, back up the description before re-raising.
2355 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002356 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002357 raise
2358
2359 if not self.GetIssue():
2360 self.SetIssue(issue)
2361 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002362 return 0
2363
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002364
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002365class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002366 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002367 # auth_config is Rietveld thing, kept here to preserve interface only.
2368 super(_GerritChangelistImpl, self).__init__(changelist)
2369 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002370 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002371 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002372 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002373 # Map from change number (issue) to its detail cache.
2374 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002375
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002376 if codereview_host is not None:
2377 assert not codereview_host.startswith('https://'), codereview_host
2378 self._gerrit_host = codereview_host
2379 self._gerrit_server = 'https://%s' % codereview_host
2380
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002381 def _GetGerritHost(self):
2382 # Lazy load of configs.
2383 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002384 if self._gerrit_host and '.' not in self._gerrit_host:
2385 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2386 # This happens for internal stuff http://crbug.com/614312.
2387 parsed = urlparse.urlparse(self.GetRemoteUrl())
2388 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002389 print('WARNING: using non-https URLs for remote is likely broken\n'
tandriie32e3ea2016-06-22 02:52:48 -07002390 ' Your current remote is: %s' % self.GetRemoteUrl())
2391 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2392 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002393 return self._gerrit_host
2394
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002395 def _GetGitHost(self):
2396 """Returns git host to be used when uploading change to Gerrit."""
2397 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2398
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002399 def GetCodereviewServer(self):
2400 if not self._gerrit_server:
2401 # If we're on a branch then get the server potentially associated
2402 # with that branch.
2403 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002404 self._gerrit_server = self._GitGetBranchConfigValue(
2405 self.CodereviewServerConfigKey())
2406 if self._gerrit_server:
2407 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002408 if not self._gerrit_server:
2409 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2410 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002411 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002412 parts[0] = parts[0] + '-review'
2413 self._gerrit_host = '.'.join(parts)
2414 self._gerrit_server = 'https://%s' % self._gerrit_host
2415 return self._gerrit_server
2416
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002417 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002418 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002419 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002420
tandrii5d48c322016-08-18 16:19:37 -07002421 @classmethod
2422 def PatchsetConfigKey(cls):
2423 return 'gerritpatchset'
2424
2425 @classmethod
2426 def CodereviewServerConfigKey(cls):
2427 return 'gerritserver'
2428
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002429 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002430 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002431 if settings.GetGerritSkipEnsureAuthenticated():
2432 # For projects with unusual authentication schemes.
2433 # See http://crbug.com/603378.
2434 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002435 # Lazy-loader to identify Gerrit and Git hosts.
2436 if gerrit_util.GceAuthenticator.is_gce():
2437 return
2438 self.GetCodereviewServer()
2439 git_host = self._GetGitHost()
2440 assert self._gerrit_server and self._gerrit_host
2441 cookie_auth = gerrit_util.CookiesAuthenticator()
2442
2443 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2444 git_auth = cookie_auth.get_auth_header(git_host)
2445 if gerrit_auth and git_auth:
2446 if gerrit_auth == git_auth:
2447 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002448 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002449 print((
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002450 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002451 ' %s\n'
2452 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002453 ' Consider running the following command:\n'
2454 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002455 ' %s\n'
Andrii Shyshkalov8e4576f2017-05-10 15:46:53 +02002456 ' %s') %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002457 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002458 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002459 cookie_auth.get_new_password_message(git_host)))
2460 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002461 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002462 return
2463 else:
2464 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02002465 ([] if gerrit_auth else [self._gerrit_host]) +
2466 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002467 DieWithError('Credentials for the following hosts are required:\n'
2468 ' %s\n'
2469 'These are read from %s (or legacy %s)\n'
2470 '%s' % (
2471 '\n '.join(missing),
2472 cookie_auth.get_gitcookies_path(),
2473 cookie_auth.get_netrc_path(),
2474 cookie_auth.get_new_password_message(git_host)))
2475
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002476 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002477 if not self.GetIssue():
2478 return
2479
2480 # Warm change details cache now to avoid RPCs later, reducing latency for
2481 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002482 self._GetChangeDetail(
2483 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002484
2485 status = self._GetChangeDetail()['status']
2486 if status in ('MERGED', 'ABANDONED'):
2487 DieWithError('Change %s has been %s, new uploads are not allowed' %
2488 (self.GetIssueURL(),
2489 'submitted' if status == 'MERGED' else 'abandoned'))
2490
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002491 if gerrit_util.GceAuthenticator.is_gce():
2492 return
2493 cookies_user = gerrit_util.CookiesAuthenticator().get_auth_email(
2494 self._GetGerritHost())
2495 if self.GetIssueOwner() == cookies_user:
2496 return
2497 logging.debug('change %s owner is %s, cookies user is %s',
2498 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002499 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002500 # so ask what Gerrit thinks of this user.
2501 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2502 if details['email'] == self.GetIssueOwner():
2503 return
2504 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002505 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002506 'as %s.\n'
2507 'Uploading may fail due to lack of permissions.' %
2508 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2509 confirm_or_exit(action='upload')
2510
2511
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002512 def _PostUnsetIssueProperties(self):
2513 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002514 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002515
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002516 def GetRietveldObjForPresubmit(self):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002517 class ThisIsNotRietveldIssue(object):
2518 def __nonzero__(self):
2519 # This is a hack to make presubmit_support think that rietveld is not
2520 # defined, yet still ensure that calls directly result in a decent
2521 # exception message below.
2522 return False
2523
2524 def __getattr__(self, attr):
2525 print(
2526 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2527 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002528 'Please, either change your PRESUBMIT to not use rietveld_obj.%s,\n'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002529 'or use Rietveld for codereview.\n'
2530 'See also http://crbug.com/579160.' % attr)
2531 raise NotImplementedError()
2532 return ThisIsNotRietveldIssue()
2533
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002534 def GetGerritObjForPresubmit(self):
2535 return presubmit_support.GerritAccessor(self._GetGerritHost())
2536
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002537 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002538 """Apply a rough heuristic to give a simple summary of an issue's review
2539 or CQ status, assuming adherence to a common workflow.
2540
2541 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002542 * 'error' - error from review tool (including deleted issues)
2543 * 'unsent' - no reviewers added
2544 * 'waiting' - waiting for review
2545 * 'reply' - waiting for uploader to reply to review
2546 * 'lgtm' - Code-Review label has been set
2547 * 'commit' - in the commit queue
2548 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002549 """
2550 if not self.GetIssue():
2551 return None
2552
2553 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002554 data = self._GetChangeDetail([
2555 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002556 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002557 return 'error'
2558
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002559 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002560 return 'closed'
2561
Aaron Gable9ab38c62017-04-06 14:36:33 -07002562 if data['labels'].get('Commit-Queue', {}).get('approved'):
2563 # The section will have an "approved" subsection if anyone has voted
2564 # the maximum value on the label.
2565 return 'commit'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002566
Aaron Gable9ab38c62017-04-06 14:36:33 -07002567 if data['labels'].get('Code-Review', {}).get('approved'):
2568 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002569
2570 if not data.get('reviewers', {}).get('REVIEWER', []):
2571 return 'unsent'
2572
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002573 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002574 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2575 last_message_author = messages.pop().get('author', {})
2576 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002577 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2578 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002579 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002580 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002581 if last_message_author.get('_account_id') == owner:
2582 # Most recent message was by owner.
2583 return 'waiting'
2584 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002585 # Some reply from non-owner.
2586 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002587
2588 # Somehow there are no messages even though there are reviewers.
2589 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002590
2591 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002592 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002593 patchset = data['revisions'][data['current_revision']]['_number']
2594 self.SetPatchset(patchset)
2595 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002596
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002597 def FetchDescription(self, force=False):
2598 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2599 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002600 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002601 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002602
dsansomee2d6fd92016-09-08 00:10:47 -07002603 def UpdateDescriptionRemote(self, description, force=False):
2604 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2605 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002606 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002607 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002608 'unpublished edit. Either publish the edit in the Gerrit web UI '
2609 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002610
2611 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2612 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002613 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002614 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002615
Aaron Gable636b13f2017-07-14 10:42:48 -07002616 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002617 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
Aaron Gable636b13f2017-07-14 10:42:48 -07002618 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002619
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002620 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002621 # DETAILED_ACCOUNTS is to get emails in accounts.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002622 messages = self._GetChangeDetail(
2623 options=['MESSAGES', 'DETAILED_ACCOUNTS']).get('messages', [])
2624 file_comments = gerrit_util.GetChangeComments(
2625 self._GetGerritHost(), self.GetIssue())
2626
2627 # Build dictionary of file comments for easy access and sorting later.
2628 # {author+date: {path: {patchset: {line: url+message}}}}
2629 comments = collections.defaultdict(
2630 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2631 for path, line_comments in file_comments.iteritems():
2632 for comment in line_comments:
2633 if comment.get('tag', '').startswith('autogenerated'):
2634 continue
2635 key = (comment['author']['email'], comment['updated'])
2636 if comment.get('side', 'REVISION') == 'PARENT':
2637 patchset = 'Base'
2638 else:
2639 patchset = 'PS%d' % comment['patch_set']
2640 line = comment.get('line', 0)
2641 url = ('https://%s/c/%s/%s/%s#%s%s' %
2642 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2643 'b' if comment.get('side') == 'PARENT' else '',
2644 str(line) if line else ''))
2645 comments[key][path][patchset][line] = (url, comment['message'])
2646
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002647 summary = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002648 for msg in messages:
2649 # Don't bother showing autogenerated messages.
2650 if msg.get('tag') and msg.get('tag').startswith('autogenerated'):
2651 continue
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002652 # Gerrit spits out nanoseconds.
2653 assert len(msg['date'].split('.')[-1]) == 9
2654 date = datetime.datetime.strptime(msg['date'][:-3],
2655 '%Y-%m-%d %H:%M:%S.%f')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002656 message = msg['message']
2657 key = (msg['author']['email'], msg['date'])
2658 if key in comments:
2659 message += '\n'
2660 for path, patchsets in sorted(comments.get(key, {}).items()):
2661 if readable:
2662 message += '\n%s' % path
2663 for patchset, lines in sorted(patchsets.items()):
2664 for line, (url, content) in sorted(lines.items()):
2665 if line:
2666 line_str = 'Line %d' % line
2667 path_str = '%s:%d:' % (path, line)
2668 else:
2669 line_str = 'File comment'
2670 path_str = '%s:0:' % path
2671 if readable:
2672 message += '\n %s, %s: %s' % (patchset, line_str, url)
2673 message += '\n %s\n' % content
2674 else:
2675 message += '\n%s ' % path_str
2676 message += '\n%s\n' % content
2677
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002678 summary.append(_CommentSummary(
2679 date=date,
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002680 message=message,
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002681 sender=msg['author']['email'],
2682 # These could be inferred from the text messages and correlated with
2683 # Code-Review label maximum, however this is not reliable.
2684 # Leaving as is until the need arises.
2685 approval=False,
2686 disapproval=False,
2687 ))
2688 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002689
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002690 def CloseIssue(self):
2691 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2692
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002693 def SubmitIssue(self, wait_for_merge=True):
2694 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2695 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002696
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002697 def _GetChangeDetail(self, options=None, issue=None,
2698 no_cache=False):
2699 """Returns details of the issue by querying Gerrit and caching results.
2700
2701 If fresh data is needed, set no_cache=True which will clear cache and
2702 thus new data will be fetched from Gerrit.
2703 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002704 options = options or []
2705 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002706 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002707
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002708 # Optimization to avoid multiple RPCs:
2709 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2710 'CURRENT_COMMIT' not in options):
2711 options.append('CURRENT_COMMIT')
2712
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002713 # Normalize issue and options for consistent keys in cache.
2714 issue = str(issue)
2715 options = [o.upper() for o in options]
2716
2717 # Check in cache first unless no_cache is True.
2718 if no_cache:
2719 self._detail_cache.pop(issue, None)
2720 else:
2721 options_set = frozenset(options)
2722 for cached_options_set, data in self._detail_cache.get(issue, []):
2723 # Assumption: data fetched before with extra options is suitable
2724 # for return for a smaller set of options.
2725 # For example, if we cached data for
2726 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2727 # and request is for options=[CURRENT_REVISION],
2728 # THEN we can return prior cached data.
2729 if options_set.issubset(cached_options_set):
2730 return data
2731
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002732 try:
Aaron Gable19ee16c2017-04-18 11:56:35 -07002733 data = gerrit_util.GetChangeDetail(
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002734 self._GetGerritHost(), str(issue), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002735 except gerrit_util.GerritError as e:
2736 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002737 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002738 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002739
2740 self._detail_cache.setdefault(issue, []).append((frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002741 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002742
agable32978d92016-11-01 12:55:02 -07002743 def _GetChangeCommit(self, issue=None):
2744 issue = issue or self.GetIssue()
2745 assert issue, 'issue is required to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002746 try:
2747 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2748 except gerrit_util.GerritError as e:
2749 if e.http_status == 404:
2750 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
2751 raise
agable32978d92016-11-01 12:55:02 -07002752 return data
2753
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002754 def CMDLand(self, force, bypass_hooks, verbose):
2755 if git_common.is_dirty_git_tree('land'):
2756 return 1
tandriid60367b2016-06-22 05:25:12 -07002757 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2758 if u'Commit-Queue' in detail.get('labels', {}):
2759 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002760 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2761 'which can test and land changes for you. '
2762 'Are you sure you wish to bypass it?\n',
2763 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002764
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002765 differs = True
tandriic4344b52016-08-29 06:04:54 -07002766 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002767 # Note: git diff outputs nothing if there is no diff.
2768 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002769 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002770 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002771 if detail['current_revision'] == last_upload:
2772 differs = False
2773 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002774 print('WARNING: Local branch contents differ from latest uploaded '
2775 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002776 if differs:
2777 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002778 confirm_or_exit(
2779 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2780 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002781 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002782 elif not bypass_hooks:
2783 hook_results = self.RunHook(
2784 committing=True,
2785 may_prompt=not force,
2786 verbose=verbose,
2787 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2788 if not hook_results.should_continue():
2789 return 1
2790
2791 self.SubmitIssue(wait_for_merge=True)
2792 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002793 links = self._GetChangeCommit().get('web_links', [])
2794 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002795 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002796 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002797 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002798 return 0
2799
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002800 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002801 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002802 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002803 assert not directory
2804 assert parsed_issue_arg.valid
2805
2806 self._changelist.issue = parsed_issue_arg.issue
2807
2808 if parsed_issue_arg.hostname:
2809 self._gerrit_host = parsed_issue_arg.hostname
2810 self._gerrit_server = 'https://%s' % self._gerrit_host
2811
tandriic2405f52016-10-10 08:13:15 -07002812 try:
2813 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002814 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002815 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002816
2817 if not parsed_issue_arg.patchset:
2818 # Use current revision by default.
2819 revision_info = detail['revisions'][detail['current_revision']]
2820 patchset = int(revision_info['_number'])
2821 else:
2822 patchset = parsed_issue_arg.patchset
2823 for revision_info in detail['revisions'].itervalues():
2824 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2825 break
2826 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002827 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002828 (parsed_issue_arg.patchset, self.GetIssue()))
2829
Aaron Gable697a91b2018-01-19 15:20:15 -08002830 remote_url = self._changelist.GetRemoteUrl()
2831 if remote_url.endswith('.git'):
2832 remote_url = remote_url[:-len('.git')]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002833 fetch_info = revision_info['fetch']['http']
Aaron Gable697a91b2018-01-19 15:20:15 -08002834
2835 if remote_url != fetch_info['url']:
2836 DieWithError('Trying to patch a change from %s but this repo appears '
2837 'to be %s.' % (fetch_info['url'], remote_url))
2838
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002839 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002840
Aaron Gable62619a32017-06-16 08:22:09 -07002841 if force:
2842 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2843 print('Checked out commit for change %i patchset %i locally' %
2844 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002845 elif nocommit:
2846 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2847 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002848 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002849 RunGit(['cherry-pick', 'FETCH_HEAD'])
2850 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002851 (parsed_issue_arg.issue, patchset))
2852 print('Note: this created a local commit which does not have '
2853 'the same hash as the one uploaded for review. This will make '
2854 'uploading changes based on top of this branch difficult.\n'
2855 'If you want to do that, use "git cl patch --force" instead.')
2856
Stefan Zagerd08043c2017-10-12 12:07:02 -07002857 if self.GetBranch():
2858 self.SetIssue(parsed_issue_arg.issue)
2859 self.SetPatchset(patchset)
2860 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2861 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2862 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2863 else:
2864 print('WARNING: You are in detached HEAD state.\n'
2865 'The patch has been applied to your checkout, but you will not be '
2866 'able to upload a new patch set to the gerrit issue.\n'
2867 'Try using the \'-b\' option if you would like to work on a '
2868 'branch and/or upload a new patch set.')
2869
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002870 return 0
2871
2872 @staticmethod
2873 def ParseIssueURL(parsed_url):
2874 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2875 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002876 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2877 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002878 # Short urls like https://domain/<issue_number> can be used, but don't allow
2879 # specifying the patchset (you'd 404), but we allow that here.
2880 if parsed_url.path == '/':
2881 part = parsed_url.fragment
2882 else:
2883 part = parsed_url.path
Aaron Gable01b91062017-08-24 17:48:40 -07002884 match = re.match('(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002885 if match:
2886 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002887 issue=int(match.group(3)),
2888 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002889 hostname=parsed_url.netloc,
2890 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002891 return None
2892
tandrii16e0b4e2016-06-07 10:34:28 -07002893 def _GerritCommitMsgHookCheck(self, offer_removal):
2894 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2895 if not os.path.exists(hook):
2896 return
2897 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2898 # custom developer made one.
2899 data = gclient_utils.FileRead(hook)
2900 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2901 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002902 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002903 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002904 'and may interfere with it in subtle ways.\n'
2905 'We recommend you remove the commit-msg hook.')
2906 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002907 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002908 gclient_utils.rm_file_or_tree(hook)
2909 print('Gerrit commit-msg hook removed.')
2910 else:
2911 print('OK, will keep Gerrit commit-msg hook in place.')
2912
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002913 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002914 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002915 if options.squash and options.no_squash:
2916 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002917
2918 if not options.squash and not options.no_squash:
2919 # Load default for user, repo, squash=true, in this order.
2920 options.squash = settings.GetSquashGerritUploads()
2921 elif options.no_squash:
2922 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002923
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002924 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002925 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002926
Aaron Gableb56ad332017-01-06 15:24:31 -08002927 # This may be None; default fallback value is determined in logic below.
2928 title = options.title
2929
Dominic Battre7d1c4842017-10-27 09:17:28 +02002930 # Extract bug number from branch name.
2931 bug = options.bug
2932 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2933 if not bug and match:
2934 bug = match.group(1)
2935
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002936 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002937 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002938 if self.GetIssue():
2939 # Try to get the message from a previous upload.
2940 message = self.GetDescription()
2941 if not message:
2942 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002943 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002944 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002945 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002946 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002947 # When uploading a subsequent patchset, -m|--message is taken
2948 # as the patchset title if --title was not provided.
2949 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002950 else:
2951 default_title = RunGit(
2952 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002953 if options.force:
2954 title = default_title
2955 else:
2956 title = ask_for_data(
2957 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002958 change_id = self._GetChangeDetail()['change_id']
2959 while True:
2960 footer_change_ids = git_footers.get_footer_change_id(message)
2961 if footer_change_ids == [change_id]:
2962 break
2963 if not footer_change_ids:
2964 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002965 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002966 continue
2967 # There is already a valid footer but with different or several ids.
2968 # Doing this automatically is non-trivial as we don't want to lose
2969 # existing other footers, yet we want to append just 1 desired
2970 # Change-Id. Thus, just create a new footer, but let user verify the
2971 # new description.
2972 message = '%s\n\nChange-Id: %s' % (message, change_id)
2973 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002974 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002975 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002976 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002977 'Please, check the proposed correction to the description, '
2978 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2979 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2980 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002981 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002982 if not options.force:
2983 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002984 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002985 message = change_desc.description
2986 if not message:
2987 DieWithError("Description is empty. Aborting...")
2988 # Continue the while loop.
2989 # Sanity check of this code - we should end up with proper message
2990 # footer.
2991 assert [change_id] == git_footers.get_footer_change_id(message)
2992 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002993 else: # if not self.GetIssue()
2994 if options.message:
2995 message = options.message
2996 else:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002997 message = CreateDescriptionFromLog(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002998 if options.title:
2999 message = options.title + '\n\n' + message
3000 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02003001
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003002 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02003003 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08003004 # On first upload, patchset title is always this string, while
3005 # --title flag gets converted to first line of message.
3006 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003007 if not change_desc.description:
3008 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02003009 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003010 if len(change_ids) > 1:
3011 DieWithError('too many Change-Id footers, at most 1 allowed.')
3012 if not change_ids:
3013 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02003014 change_desc.set_description(git_footers.add_footer_change_id(
3015 change_desc.description,
3016 GenerateGerritChangeId(change_desc.description)))
3017 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003018 assert len(change_ids) == 1
3019 change_id = change_ids[0]
3020
Robert Iannuccidb02dd02017-04-19 12:18:20 -07003021 if options.reviewers or options.tbrs or options.add_owners_to:
3022 change_desc.update_reviewers(options.reviewers, options.tbrs,
3023 options.add_owners_to, change)
3024
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003025 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003026 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
3027 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003028 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07003029 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
3030 desc_tempfile.write(change_desc.description)
3031 desc_tempfile.close()
3032 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
3033 '-F', desc_tempfile.name]).strip()
3034 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003035 else:
3036 change_desc = ChangeDescription(
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003037 options.message or CreateDescriptionFromLog(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003038 if not change_desc.description:
3039 DieWithError("Description is empty. Aborting...")
3040
3041 if not git_footers.get_footer_change_id(change_desc.description):
3042 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003043 change_desc.set_description(
3044 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07003045 if options.reviewers or options.tbrs or options.add_owners_to:
3046 change_desc.update_reviewers(options.reviewers, options.tbrs,
3047 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003048 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003049 # For no-squash mode, we assume the remote called "origin" is the one we
3050 # want. It is not worthwhile to support different workflows for
3051 # no-squash mode.
3052 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003053 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
3054
3055 assert change_desc
3056 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
3057 ref_to_push)]).splitlines()
3058 if len(commits) > 1:
3059 print('WARNING: This will upload %d commits. Run the following command '
3060 'to see which commits will be uploaded: ' % len(commits))
3061 print('git log %s..%s' % (parent, ref_to_push))
3062 print('You can also use `git squash-branch` to squash these into a '
3063 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003064 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003065
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003066 if options.reviewers or options.tbrs or options.add_owners_to:
3067 change_desc.update_reviewers(options.reviewers, options.tbrs,
3068 options.add_owners_to, change)
3069
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00003070 # Extra options that can be specified at push time. Doc:
3071 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003072 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003073
Aaron Gable844cf292017-06-28 11:32:59 -07003074 # By default, new changes are started in WIP mode, and subsequent patchsets
3075 # don't send email. At any time, passing --send-mail will mark the change
3076 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07003077 if options.send_mail:
3078 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07003079 refspec_opts.append('notify=ALL')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003080 elif not self.GetIssue():
3081 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07003082 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003083 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07003084
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003085 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07003086 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003087
Aaron Gable9b713dd2016-12-14 16:04:21 -08003088 if title:
Nick Carter8692b182017-11-06 16:30:38 -08003089 # Punctuation and whitespace in |title| must be percent-encoded.
3090 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00003091
agablec6787972016-09-09 16:13:34 -07003092 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07003093 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07003094
rmistry9eadede2016-09-19 11:22:43 -07003095 if options.topic:
3096 # Documentation on Gerrit topics is here:
3097 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003098 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07003099
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003100 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08003101 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003102 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08003103 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003104 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
3105
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003106 refspec_suffix = ''
3107 if refspec_opts:
3108 refspec_suffix = '%' + ','.join(refspec_opts)
3109 assert ' ' not in refspec_suffix, (
3110 'spaces not allowed in refspec: "%s"' % refspec_suffix)
3111 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
3112
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003113 try:
3114 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003115 ['git', 'push', self.GetRemoteUrl(), refspec],
3116 print_stdout=True,
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003117 # Flush after every line: useful for seeing progress when running as
3118 # recipe.
3119 filter_fn=lambda _: sys.stdout.flush())
3120 except subprocess2.CalledProcessError:
3121 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003122 'for the reason of the failure.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003123 'Hint: run command below to diagnose common Git/Gerrit '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003124 'credential problems:\n'
3125 ' git cl creds-check\n',
3126 change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003127
3128 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07003129 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003130 change_numbers = [m.group(1)
3131 for m in map(regex.match, push_stdout.splitlines())
3132 if m]
3133 if len(change_numbers) != 1:
3134 DieWithError(
3135 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11003136 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003137 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07003138 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07003139
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003140 reviewers = sorted(change_desc.get_reviewers())
3141
tandrii88189772016-09-29 04:29:57 -07003142 # Add cc's from the CC_LIST and --cc flag (if any).
Aaron Gabled1052492017-05-15 15:05:34 -07003143 if not options.private:
3144 cc = self.GetCCList().split(',')
3145 else:
3146 cc = []
tandrii88189772016-09-29 04:29:57 -07003147 if options.cc:
3148 cc.extend(options.cc)
3149 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07003150 if change_desc.get_cced():
3151 cc.extend(change_desc.get_cced())
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003152
3153 gerrit_util.AddReviewers(
3154 self._GetGerritHost(), self.GetIssue(), reviewers, cc,
3155 notify=bool(options.send_mail))
3156
Aaron Gablefd238082017-06-07 13:42:34 -07003157 if change_desc.get_reviewers(tbr_only=True):
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003158 labels = self._GetChangeDetail(['LABELS']).get('labels', {})
3159 score = 1
3160 if 'Code-Review' in labels and 'values' in labels['Code-Review']:
3161 score = max([int(x) for x in labels['Code-Review']['values'].keys()])
3162 print('Adding self-LGTM (Code-Review +%d) because of TBRs.' % score)
Aaron Gablefd238082017-06-07 13:42:34 -07003163 gerrit_util.SetReview(
3164 self._GetGerritHost(), self.GetIssue(),
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003165 msg='Self-approving for TBR',
3166 labels={'Code-Review': score})
Aaron Gablefd238082017-06-07 13:42:34 -07003167
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003168 return 0
3169
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003170 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
3171 change_desc):
3172 """Computes parent of the generated commit to be uploaded to Gerrit.
3173
3174 Returns revision or a ref name.
3175 """
3176 if custom_cl_base:
3177 # Try to avoid creating additional unintended CLs when uploading, unless
3178 # user wants to take this risk.
3179 local_ref_of_target_remote = self.GetRemoteBranch()[1]
3180 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
3181 local_ref_of_target_remote])
3182 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003183 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003184 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
3185 'If you proceed with upload, more than 1 CL may be created by '
3186 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
3187 'If you are certain that specified base `%s` has already been '
3188 'uploaded to Gerrit as another CL, you may proceed.\n' %
3189 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
3190 if not force:
3191 confirm_or_exit(
3192 'Do you take responsibility for cleaning up potential mess '
3193 'resulting from proceeding with upload?',
3194 action='upload')
3195 return custom_cl_base
3196
Aaron Gablef97e33d2017-03-30 15:44:27 -07003197 if remote != '.':
3198 return self.GetCommonAncestorWithUpstream()
3199
3200 # If our upstream branch is local, we base our squashed commit on its
3201 # squashed version.
3202 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
3203
Aaron Gablef97e33d2017-03-30 15:44:27 -07003204 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07003205 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003206
3207 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003208 # TODO(tandrii): consider checking parent change in Gerrit and using its
3209 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
3210 # the tree hash of the parent branch. The upside is less likely bogus
3211 # requests to reupload parent change just because it's uploadhash is
3212 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07003213 parent = RunGit(['config',
3214 'branch.%s.gerritsquashhash' % upstream_branch_name],
3215 error_ok=True).strip()
3216 # Verify that the upstream branch has been uploaded too, otherwise
3217 # Gerrit will create additional CLs when uploading.
3218 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
3219 RunGitSilent(['rev-parse', parent + ':'])):
3220 DieWithError(
3221 '\nUpload upstream branch %s first.\n'
3222 'It is likely that this branch has been rebased since its last '
3223 'upload, so you just need to upload it again.\n'
3224 '(If you uploaded it with --no-squash, then branch dependencies '
3225 'are not supported, and you should reupload with --squash.)'
3226 % upstream_branch_name,
3227 change_desc)
3228 return parent
3229
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003230 def _AddChangeIdToCommitMessage(self, options, args):
3231 """Re-commits using the current message, assumes the commit hook is in
3232 place.
3233 """
3234 log_desc = options.message or CreateDescriptionFromLog(args)
3235 git_command = ['commit', '--amend', '-m', log_desc]
3236 RunGit(git_command)
3237 new_log_desc = CreateDescriptionFromLog(args)
3238 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003239 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003240 return new_log_desc
3241 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00003242 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003243
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003244 def SetCQState(self, new_state):
3245 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003246 vote_map = {
3247 _CQState.NONE: 0,
3248 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003249 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003250 }
Aaron Gablefc62f762017-07-17 11:12:07 -07003251 labels = {'Commit-Queue': vote_map[new_state]}
3252 notify = False if new_state == _CQState.DRY_RUN else None
3253 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
3254 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003255
tandriie113dfd2016-10-11 10:20:12 -07003256 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003257 try:
3258 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003259 except GerritChangeNotExists:
3260 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003261
3262 if data['status'] in ('ABANDONED', 'MERGED'):
3263 return 'CL %s is closed' % self.GetIssue()
3264
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003265 def GetTryJobProperties(self, patchset=None):
3266 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07003267 data = self._GetChangeDetail(['ALL_REVISIONS'])
3268 patchset = int(patchset or self.GetPatchset())
3269 assert patchset
3270 revision_data = None # Pylint wants it to be defined.
3271 for revision_data in data['revisions'].itervalues():
3272 if int(revision_data['_number']) == patchset:
3273 break
3274 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003275 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003276 (patchset, self.GetIssue()))
3277 return {
3278 'patch_issue': self.GetIssue(),
3279 'patch_set': patchset or self.GetPatchset(),
3280 'patch_project': data['project'],
3281 'patch_storage': 'gerrit',
3282 'patch_ref': revision_data['fetch']['http']['ref'],
3283 'patch_repository_url': revision_data['fetch']['http']['url'],
3284 'patch_gerrit_url': self.GetCodereviewServer(),
3285 }
tandriie113dfd2016-10-11 10:20:12 -07003286
tandriide281ae2016-10-12 06:02:30 -07003287 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003288 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003289
Edward Lemur707d70b2018-02-07 00:50:14 +01003290 def GetReviewers(self):
3291 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
3292 return [reviewer['email'] for reviewer in details['reviewers']['REVIEWER']]
3293
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003294
3295_CODEREVIEW_IMPLEMENTATIONS = {
3296 'rietveld': _RietveldChangelistImpl,
3297 'gerrit': _GerritChangelistImpl,
3298}
3299
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003300
iannuccie53c9352016-08-17 14:40:40 -07003301def _add_codereview_issue_select_options(parser, extra=""):
3302 _add_codereview_select_options(parser)
3303
3304 text = ('Operate on this issue number instead of the current branch\'s '
3305 'implicit issue.')
3306 if extra:
3307 text += ' '+extra
3308 parser.add_option('-i', '--issue', type=int, help=text)
3309
3310
3311def _process_codereview_issue_select_options(parser, options):
3312 _process_codereview_select_options(parser, options)
3313 if options.issue is not None and not options.forced_codereview:
3314 parser.error('--issue must be specified with either --rietveld or --gerrit')
3315
3316
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003317def _add_codereview_select_options(parser):
3318 """Appends --gerrit and --rietveld options to force specific codereview."""
3319 parser.codereview_group = optparse.OptionGroup(
3320 parser, 'EXPERIMENTAL! Codereview override options')
3321 parser.add_option_group(parser.codereview_group)
3322 parser.codereview_group.add_option(
3323 '--gerrit', action='store_true',
3324 help='Force the use of Gerrit for codereview')
3325 parser.codereview_group.add_option(
3326 '--rietveld', action='store_true',
3327 help='Force the use of Rietveld for codereview')
3328
3329
3330def _process_codereview_select_options(parser, options):
3331 if options.gerrit and options.rietveld:
3332 parser.error('Options --gerrit and --rietveld are mutually exclusive')
3333 options.forced_codereview = None
3334 if options.gerrit:
3335 options.forced_codereview = 'gerrit'
3336 elif options.rietveld:
3337 options.forced_codereview = 'rietveld'
3338
3339
tandriif9aefb72016-07-01 09:06:51 -07003340def _get_bug_line_values(default_project, bugs):
3341 """Given default_project and comma separated list of bugs, yields bug line
3342 values.
3343
3344 Each bug can be either:
3345 * a number, which is combined with default_project
3346 * string, which is left as is.
3347
3348 This function may produce more than one line, because bugdroid expects one
3349 project per line.
3350
3351 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3352 ['v8:123', 'chromium:789']
3353 """
3354 default_bugs = []
3355 others = []
3356 for bug in bugs.split(','):
3357 bug = bug.strip()
3358 if bug:
3359 try:
3360 default_bugs.append(int(bug))
3361 except ValueError:
3362 others.append(bug)
3363
3364 if default_bugs:
3365 default_bugs = ','.join(map(str, default_bugs))
3366 if default_project:
3367 yield '%s:%s' % (default_project, default_bugs)
3368 else:
3369 yield default_bugs
3370 for other in sorted(others):
3371 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3372 yield other
3373
3374
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003375class ChangeDescription(object):
3376 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003377 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003378 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003379 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003380 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003381 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3382 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
3383 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
3384 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003385
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003386 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003387 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003388
agable@chromium.org42c20792013-09-12 17:34:49 +00003389 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003390 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003391 return '\n'.join(self._description_lines)
3392
3393 def set_description(self, desc):
3394 if isinstance(desc, basestring):
3395 lines = desc.splitlines()
3396 else:
3397 lines = [line.rstrip() for line in desc]
3398 while lines and not lines[0]:
3399 lines.pop(0)
3400 while lines and not lines[-1]:
3401 lines.pop(-1)
3402 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003403
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003404 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3405 """Rewrites the R=/TBR= line(s) as a single line each.
3406
3407 Args:
3408 reviewers (list(str)) - list of additional emails to use for reviewers.
3409 tbrs (list(str)) - list of additional emails to use for TBRs.
3410 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3411 the change that are missing OWNER coverage. If this is not None, you
3412 must also pass a value for `change`.
3413 change (Change) - The Change that should be used for OWNERS lookups.
3414 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003415 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003416 assert isinstance(tbrs, list), tbrs
3417
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003418 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003419 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003420
3421 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003422 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003423
3424 reviewers = set(reviewers)
3425 tbrs = set(tbrs)
3426 LOOKUP = {
3427 'TBR': tbrs,
3428 'R': reviewers,
3429 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003430
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003431 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003432 regexp = re.compile(self.R_LINE)
3433 matches = [regexp.match(line) for line in self._description_lines]
3434 new_desc = [l for i, l in enumerate(self._description_lines)
3435 if not matches[i]]
3436 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003437
agable@chromium.org42c20792013-09-12 17:34:49 +00003438 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003439
3440 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003441 for match in matches:
3442 if not match:
3443 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003444 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3445
3446 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003447 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003448 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003449 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003450 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003451 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003452 LOOKUP[add_owners_to].update(
3453 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003454
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003455 # If any folks ended up in both groups, remove them from tbrs.
3456 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003457
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003458 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3459 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003460
3461 # Put the new lines in the description where the old first R= line was.
3462 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3463 if 0 <= line_loc < len(self._description_lines):
3464 if new_tbr_line:
3465 self._description_lines.insert(line_loc, new_tbr_line)
3466 if new_r_line:
3467 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003468 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003469 if new_r_line:
3470 self.append_footer(new_r_line)
3471 if new_tbr_line:
3472 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003473
Aaron Gable3a16ed12017-03-23 10:51:55 -07003474 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003475 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003476 self.set_description([
3477 '# Enter a description of the change.',
3478 '# This will be displayed on the codereview site.',
3479 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003480 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003481 '--------------------',
3482 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003483
agable@chromium.org42c20792013-09-12 17:34:49 +00003484 regexp = re.compile(self.BUG_LINE)
3485 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003486 prefix = settings.GetBugPrefix()
3487 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003488 if git_footer:
3489 self.append_footer('Bug: %s' % ', '.join(values))
3490 else:
3491 for value in values:
3492 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003493
agable@chromium.org42c20792013-09-12 17:34:49 +00003494 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003495 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003496 if not content:
3497 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003498 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003499
Bruce Dawson2377b012018-01-11 16:46:49 -08003500 # Strip off comments and default inserted "Bug:" line.
3501 clean_lines = [line.rstrip() for line in lines if not
3502 (line.startswith('#') or line.rstrip() == "Bug:")]
agable@chromium.org42c20792013-09-12 17:34:49 +00003503 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003504 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003505 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003506
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003507 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003508 """Adds a footer line to the description.
3509
3510 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3511 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3512 that Gerrit footers are always at the end.
3513 """
3514 parsed_footer_line = git_footers.parse_footer(line)
3515 if parsed_footer_line:
3516 # Line is a gerrit footer in the form: Footer-Key: any value.
3517 # Thus, must be appended observing Gerrit footer rules.
3518 self.set_description(
3519 git_footers.add_footer(self.description,
3520 key=parsed_footer_line[0],
3521 value=parsed_footer_line[1]))
3522 return
3523
3524 if not self._description_lines:
3525 self._description_lines.append(line)
3526 return
3527
3528 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3529 if gerrit_footers:
3530 # git_footers.split_footers ensures that there is an empty line before
3531 # actual (gerrit) footers, if any. We have to keep it that way.
3532 assert top_lines and top_lines[-1] == ''
3533 top_lines, separator = top_lines[:-1], top_lines[-1:]
3534 else:
3535 separator = [] # No need for separator if there are no gerrit_footers.
3536
3537 prev_line = top_lines[-1] if top_lines else ''
3538 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3539 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3540 top_lines.append('')
3541 top_lines.append(line)
3542 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003543
tandrii99a72f22016-08-17 14:33:24 -07003544 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003545 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003546 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003547 reviewers = [match.group(2).strip()
3548 for match in matches
3549 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003550 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003551
bradnelsond975b302016-10-23 12:20:23 -07003552 def get_cced(self):
3553 """Retrieves the list of reviewers."""
3554 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3555 cced = [match.group(2).strip() for match in matches if match]
3556 return cleanup_list(cced)
3557
Nodir Turakulov23b82142017-11-16 11:04:25 -08003558 def get_hash_tags(self):
3559 """Extracts and sanitizes a list of Gerrit hashtags."""
3560 subject = (self._description_lines or ('',))[0]
3561 subject = re.sub(
3562 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3563
3564 tags = []
3565 start = 0
3566 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3567 while True:
3568 m = bracket_exp.match(subject, start)
3569 if not m:
3570 break
3571 tags.append(self.sanitize_hash_tag(m.group(1)))
3572 start = m.end()
3573
3574 if not tags:
3575 # Try "Tag: " prefix.
3576 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3577 if m:
3578 tags.append(self.sanitize_hash_tag(m.group(1)))
3579 return tags
3580
3581 @classmethod
3582 def sanitize_hash_tag(cls, tag):
3583 """Returns a sanitized Gerrit hash tag.
3584
3585 A sanitized hashtag can be used as a git push refspec parameter value.
3586 """
3587 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3588
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003589 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3590 """Updates this commit description given the parent.
3591
3592 This is essentially what Gnumbd used to do.
3593 Consult https://goo.gl/WMmpDe for more details.
3594 """
3595 assert parent_msg # No, orphan branch creation isn't supported.
3596 assert parent_hash
3597 assert dest_ref
3598 parent_footer_map = git_footers.parse_footers(parent_msg)
3599 # This will also happily parse svn-position, which GnumbD is no longer
3600 # supporting. While we'd generate correct footers, the verifier plugin
3601 # installed in Gerrit will block such commit (ie git push below will fail).
3602 parent_position = git_footers.get_position(parent_footer_map)
3603
3604 # Cherry-picks may have last line obscuring their prior footers,
3605 # from git_footers perspective. This is also what Gnumbd did.
3606 cp_line = None
3607 if (self._description_lines and
3608 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3609 cp_line = self._description_lines.pop()
3610
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003611 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003612
3613 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3614 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003615 for i, line in enumerate(footer_lines):
3616 k, v = git_footers.parse_footer(line) or (None, None)
3617 if k and k.startswith('Cr-'):
3618 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003619
3620 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003621 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003622 if parent_position[0] == dest_ref:
3623 # Same branch as parent.
3624 number = int(parent_position[1]) + 1
3625 else:
3626 number = 1 # New branch, and extra lineage.
3627 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3628 int(parent_position[1])))
3629
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003630 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3631 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003632
3633 self._description_lines = top_lines
3634 if cp_line:
3635 self._description_lines.append(cp_line)
3636 if self._description_lines[-1] != '':
3637 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003638 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003639
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003640
Aaron Gablea1bab272017-04-11 16:38:18 -07003641def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003642 """Retrieves the reviewers that approved a CL from the issue properties with
3643 messages.
3644
3645 Note that the list may contain reviewers that are not committer, thus are not
3646 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003647
3648 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003649 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003650 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003651 return sorted(
3652 set(
3653 message['sender']
3654 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003655 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003656 )
3657 )
3658
3659
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003660def FindCodereviewSettingsFile(filename='codereview.settings'):
3661 """Finds the given file starting in the cwd and going up.
3662
3663 Only looks up to the top of the repository unless an
3664 'inherit-review-settings-ok' file exists in the root of the repository.
3665 """
3666 inherit_ok_file = 'inherit-review-settings-ok'
3667 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003668 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003669 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3670 root = '/'
3671 while True:
3672 if filename in os.listdir(cwd):
3673 if os.path.isfile(os.path.join(cwd, filename)):
3674 return open(os.path.join(cwd, filename))
3675 if cwd == root:
3676 break
3677 cwd = os.path.dirname(cwd)
3678
3679
3680def LoadCodereviewSettingsFromFile(fileobj):
3681 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003682 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003683
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003684 def SetProperty(name, setting, unset_error_ok=False):
3685 fullname = 'rietveld.' + name
3686 if setting in keyvals:
3687 RunGit(['config', fullname, keyvals[setting]])
3688 else:
3689 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3690
tandrii48df5812016-10-17 03:55:37 -07003691 if not keyvals.get('GERRIT_HOST', False):
3692 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003693 # Only server setting is required. Other settings can be absent.
3694 # In that case, we ignore errors raised during option deletion attempt.
3695 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003696 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003697 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3698 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003699 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003700 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3701 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003702 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003703 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3704 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003705
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003706 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003707 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003708
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003709 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003710 RunGit(['config', 'gerrit.squash-uploads',
3711 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003712
tandrii@chromium.org28253532016-04-14 13:46:56 +00003713 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003714 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003715 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3716
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003717 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003718 # should be of the form
3719 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3720 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003721 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3722 keyvals['ORIGIN_URL_CONFIG']])
3723
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003724
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003725def urlretrieve(source, destination):
3726 """urllib is broken for SSL connections via a proxy therefore we
3727 can't use urllib.urlretrieve()."""
3728 with open(destination, 'w') as f:
3729 f.write(urllib2.urlopen(source).read())
3730
3731
ukai@chromium.org712d6102013-11-27 00:52:58 +00003732def hasSheBang(fname):
3733 """Checks fname is a #! script."""
3734 with open(fname) as f:
3735 return f.read(2).startswith('#!')
3736
3737
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003738# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3739def DownloadHooks(*args, **kwargs):
3740 pass
3741
3742
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003743def DownloadGerritHook(force):
3744 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003745
3746 Args:
3747 force: True to update hooks. False to install hooks if not present.
3748 """
3749 if not settings.GetIsGerrit():
3750 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003751 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003752 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3753 if not os.access(dst, os.X_OK):
3754 if os.path.exists(dst):
3755 if not force:
3756 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003757 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003758 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003759 if not hasSheBang(dst):
3760 DieWithError('Not a script: %s\n'
3761 'You need to download from\n%s\n'
3762 'into .git/hooks/commit-msg and '
3763 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003764 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3765 except Exception:
3766 if os.path.exists(dst):
3767 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003768 DieWithError('\nFailed to download hooks.\n'
3769 'You need to download from\n%s\n'
3770 'into .git/hooks/commit-msg and '
3771 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003772
3773
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003774def GetRietveldCodereviewSettingsInteractively():
3775 """Prompt the user for settings."""
3776 server = settings.GetDefaultServerUrl(error_ok=True)
3777 prompt = 'Rietveld server (host[:port])'
3778 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3779 newserver = ask_for_data(prompt + ':')
3780 if not server and not newserver:
3781 newserver = DEFAULT_SERVER
3782 if newserver:
3783 newserver = gclient_utils.UpgradeToHttps(newserver)
3784 if newserver != server:
3785 RunGit(['config', 'rietveld.server', newserver])
3786
3787 def SetProperty(initial, caption, name, is_url):
3788 prompt = caption
3789 if initial:
3790 prompt += ' ("x" to clear) [%s]' % initial
3791 new_val = ask_for_data(prompt + ':')
3792 if new_val == 'x':
3793 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3794 elif new_val:
3795 if is_url:
3796 new_val = gclient_utils.UpgradeToHttps(new_val)
3797 if new_val != initial:
3798 RunGit(['config', 'rietveld.' + name, new_val])
3799
3800 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3801 SetProperty(settings.GetDefaultPrivateFlag(),
3802 'Private flag (rietveld only)', 'private', False)
3803 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3804 'tree-status-url', False)
3805 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3806 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3807 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3808 'run-post-upload-hook', False)
3809
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003810
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003811class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003812 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003813
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003814 _GOOGLESOURCE = 'googlesource.com'
3815
3816 def __init__(self):
3817 # Cached list of [host, identity, source], where source is either
3818 # .gitcookies or .netrc.
3819 self._all_hosts = None
3820
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003821 def ensure_configured_gitcookies(self):
3822 """Runs checks and suggests fixes to make git use .gitcookies from default
3823 path."""
3824 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3825 configured_path = RunGitSilent(
3826 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003827 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003828 if configured_path:
3829 self._ensure_default_gitcookies_path(configured_path, default)
3830 else:
3831 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003832
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003833 @staticmethod
3834 def _ensure_default_gitcookies_path(configured_path, default_path):
3835 assert configured_path
3836 if configured_path == default_path:
3837 print('git is already configured to use your .gitcookies from %s' %
3838 configured_path)
3839 return
3840
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003841 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003842 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3843 (configured_path, default_path))
3844
3845 if not os.path.exists(configured_path):
3846 print('However, your configured .gitcookies file is missing.')
3847 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3848 action='reconfigure')
3849 RunGit(['config', '--global', 'http.cookiefile', default_path])
3850 return
3851
3852 if os.path.exists(default_path):
3853 print('WARNING: default .gitcookies file already exists %s' %
3854 default_path)
3855 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3856 default_path)
3857
3858 confirm_or_exit('Move existing .gitcookies to default location?',
3859 action='move')
3860 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003861 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003862 print('Moved and reconfigured git to use .gitcookies from %s' %
3863 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003864
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003865 @staticmethod
3866 def _configure_gitcookies_path(default_path):
3867 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3868 if os.path.exists(netrc_path):
3869 print('You seem to be using outdated .netrc for git credentials: %s' %
3870 netrc_path)
3871 print('This tool will guide you through setting up recommended '
3872 '.gitcookies store for git credentials.\n'
3873 '\n'
3874 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3875 ' git config --global --unset http.cookiefile\n'
3876 ' mv %s %s.backup\n\n' % (default_path, default_path))
3877 confirm_or_exit(action='setup .gitcookies')
3878 RunGit(['config', '--global', 'http.cookiefile', default_path])
3879 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003880
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003881 def get_hosts_with_creds(self, include_netrc=False):
3882 if self._all_hosts is None:
3883 a = gerrit_util.CookiesAuthenticator()
3884 self._all_hosts = [
3885 (h, u, s)
3886 for h, u, s in itertools.chain(
3887 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3888 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3889 )
3890 if h.endswith(self._GOOGLESOURCE)
3891 ]
3892
3893 if include_netrc:
3894 return self._all_hosts
3895 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3896
3897 def print_current_creds(self, include_netrc=False):
3898 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3899 if not hosts:
3900 print('No Git/Gerrit credentials found')
3901 return
3902 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3903 header = [('Host', 'User', 'Which file'),
3904 ['=' * l for l in lengths]]
3905 for row in (header + hosts):
3906 print('\t'.join((('%%+%ds' % l) % s)
3907 for l, s in zip(lengths, row)))
3908
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003909 @staticmethod
3910 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003911 """Parses identity "git-<username>.domain" into <username> and domain."""
3912 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003913 # distinguishable from sub-domains. But we do know typical domains:
3914 if identity.endswith('.chromium.org'):
3915 domain = 'chromium.org'
3916 username = identity[:-len('.chromium.org')]
3917 else:
3918 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003919 if username.startswith('git-'):
3920 username = username[len('git-'):]
3921 return username, domain
3922
3923 def _get_usernames_of_domain(self, domain):
3924 """Returns list of usernames referenced by .gitcookies in a given domain."""
3925 identities_by_domain = {}
3926 for _, identity, _ in self.get_hosts_with_creds():
3927 username, domain = self._parse_identity(identity)
3928 identities_by_domain.setdefault(domain, []).append(username)
3929 return identities_by_domain.get(domain)
3930
3931 def _canonical_git_googlesource_host(self, host):
3932 """Normalizes Gerrit hosts (with '-review') to Git host."""
3933 assert host.endswith(self._GOOGLESOURCE)
3934 # Prefix doesn't include '.' at the end.
3935 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3936 if prefix.endswith('-review'):
3937 prefix = prefix[:-len('-review')]
3938 return prefix + '.' + self._GOOGLESOURCE
3939
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003940 def _canonical_gerrit_googlesource_host(self, host):
3941 git_host = self._canonical_git_googlesource_host(host)
3942 prefix = git_host.split('.', 1)[0]
3943 return prefix + '-review.' + self._GOOGLESOURCE
3944
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003945 def _get_counterpart_host(self, host):
3946 assert host.endswith(self._GOOGLESOURCE)
3947 git = self._canonical_git_googlesource_host(host)
3948 gerrit = self._canonical_gerrit_googlesource_host(git)
3949 return git if gerrit == host else gerrit
3950
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003951 def has_generic_host(self):
3952 """Returns whether generic .googlesource.com has been configured.
3953
3954 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3955 """
3956 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3957 if host == '.' + self._GOOGLESOURCE:
3958 return True
3959 return False
3960
3961 def _get_git_gerrit_identity_pairs(self):
3962 """Returns map from canonic host to pair of identities (Git, Gerrit).
3963
3964 One of identities might be None, meaning not configured.
3965 """
3966 host_to_identity_pairs = {}
3967 for host, identity, _ in self.get_hosts_with_creds():
3968 canonical = self._canonical_git_googlesource_host(host)
3969 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3970 idx = 0 if canonical == host else 1
3971 pair[idx] = identity
3972 return host_to_identity_pairs
3973
3974 def get_partially_configured_hosts(self):
3975 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003976 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3977 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3978 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003979
3980 def get_conflicting_hosts(self):
3981 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003982 host
3983 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003984 if None not in (i1, i2) and i1 != i2)
3985
3986 def get_duplicated_hosts(self):
3987 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3988 return set(host for host, count in counters.iteritems() if count > 1)
3989
3990 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3991 'chromium.googlesource.com': 'chromium.org',
3992 'chrome-internal.googlesource.com': 'google.com',
3993 }
3994
3995 def get_hosts_with_wrong_identities(self):
3996 """Finds hosts which **likely** reference wrong identities.
3997
3998 Note: skips hosts which have conflicting identities for Git and Gerrit.
3999 """
4000 hosts = set()
4001 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
4002 pair = self._get_git_gerrit_identity_pairs().get(host)
4003 if pair and pair[0] == pair[1]:
4004 _, domain = self._parse_identity(pair[0])
4005 if domain != expected:
4006 hosts.add(host)
4007 return hosts
4008
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004009 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004010 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004011 hosts = sorted(hosts)
4012 assert hosts
4013 if extra_column_func is None:
4014 extras = [''] * len(hosts)
4015 else:
4016 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004017 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
4018 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004019 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004020 lines.append(tmpl % he)
4021 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004022
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004023 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004024 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004025 yield ('.googlesource.com wildcard record detected',
4026 ['Chrome Infrastructure team recommends to list full host names '
4027 'explicitly.'],
4028 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004029
4030 dups = self.get_duplicated_hosts()
4031 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004032 yield ('The following hosts were defined twice',
4033 self._format_hosts(dups),
4034 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004035
4036 partial = self.get_partially_configured_hosts()
4037 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004038 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
4039 'These hosts are missing',
4040 self._format_hosts(partial, lambda host: 'but %s defined' %
4041 self._get_counterpart_host(host)),
4042 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004043
4044 conflicting = self.get_conflicting_hosts()
4045 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004046 yield ('The following Git hosts have differing credentials from their '
4047 'Gerrit counterparts',
4048 self._format_hosts(conflicting, lambda host: '%s vs %s' %
4049 tuple(self._get_git_gerrit_identity_pairs()[host])),
4050 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004051
4052 wrong = self.get_hosts_with_wrong_identities()
4053 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004054 yield ('These hosts likely use wrong identity',
4055 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
4056 (self._get_git_gerrit_identity_pairs()[host][0],
4057 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
4058 wrong)
4059
4060 def find_and_report_problems(self):
4061 """Returns True if there was at least one problem, else False."""
4062 found = False
4063 bad_hosts = set()
4064 for title, sublines, hosts in self._find_problems():
4065 if not found:
4066 found = True
4067 print('\n\n.gitcookies problem report:\n')
4068 bad_hosts.update(hosts or [])
4069 print(' %s%s' % (title , (':' if sublines else '')))
4070 if sublines:
4071 print()
4072 print(' %s' % '\n '.join(sublines))
4073 print()
4074
4075 if bad_hosts:
4076 assert found
4077 print(' You can manually remove corresponding lines in your %s file and '
4078 'visit the following URLs with correct account to generate '
4079 'correct credential lines:\n' %
4080 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
4081 print(' %s' % '\n '.join(sorted(set(
4082 gerrit_util.CookiesAuthenticator().get_new_password_url(
4083 self._canonical_git_googlesource_host(host))
4084 for host in bad_hosts
4085 ))))
4086 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004087
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004088
4089def CMDcreds_check(parser, args):
4090 """Checks credentials and suggests changes."""
4091 _, _ = parser.parse_args(args)
4092
4093 if gerrit_util.GceAuthenticator.is_gce():
Aaron Gabled10ca0e2017-09-11 11:24:10 -07004094 DieWithError(
4095 'This command is not designed for GCE, are you on a bot?\n'
4096 'If you need to run this, export SKIP_GCE_AUTH_FOR_GIT=1 in your env.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004097
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01004098 checker = _GitCookiesChecker()
4099 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004100
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004101 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004102 checker.print_current_creds(include_netrc=True)
4103
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004104 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004105 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004106 return 0
4107 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004108
4109
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004110@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004111def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004112 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004113
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004114 print('WARNING: git cl config works for Rietveld only.')
tandrii5d0a0422016-09-14 06:24:35 -07004115 # TODO(tandrii): remove this once we switch to Gerrit.
4116 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00004117 parser.add_option('--activate-update', action='store_true',
4118 help='activate auto-updating [rietveld] section in '
4119 '.git/config')
4120 parser.add_option('--deactivate-update', action='store_true',
4121 help='deactivate auto-updating [rietveld] section in '
4122 '.git/config')
4123 options, args = parser.parse_args(args)
4124
4125 if options.deactivate_update:
4126 RunGit(['config', 'rietveld.autoupdate', 'false'])
4127 return
4128
4129 if options.activate_update:
4130 RunGit(['config', '--unset', 'rietveld.autoupdate'])
4131 return
4132
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004133 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00004134 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004135 return 0
4136
4137 url = args[0]
4138 if not url.endswith('codereview.settings'):
4139 url = os.path.join(url, 'codereview.settings')
4140
4141 # Load code review settings and download hooks (if available).
4142 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
4143 return 0
4144
4145
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004146def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004147 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004148 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
4149 branch = ShortBranchName(branchref)
4150 _, args = parser.parse_args(args)
4151 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07004152 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004153 return RunGit(['config', 'branch.%s.base-url' % branch],
4154 error_ok=False).strip()
4155 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004156 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004157 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
4158 error_ok=False).strip()
4159
4160
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004161def color_for_status(status):
4162 """Maps a Changelist status to color, for CMDstatus and other tools."""
4163 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07004164 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004165 'waiting': Fore.BLUE,
4166 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07004167 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004168 'lgtm': Fore.GREEN,
4169 'commit': Fore.MAGENTA,
4170 'closed': Fore.CYAN,
4171 'error': Fore.WHITE,
4172 }.get(status, Fore.WHITE)
4173
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00004174
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004175def get_cl_statuses(changes, fine_grained, max_processes=None):
4176 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004177
4178 If fine_grained is true, this will fetch CL statuses from the server.
4179 Otherwise, simply indicate if there's a matching url for the given branches.
4180
4181 If max_processes is specified, it is used as the maximum number of processes
4182 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
4183 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004184
4185 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004186 """
qyearsley12fa6ff2016-08-24 09:18:40 -07004187 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004188 upload.verbosity = 0
4189
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004190 if not changes:
4191 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004192
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004193 if not fine_grained:
4194 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07004195 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004196 for cl in changes:
4197 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004198 return
4199
4200 # First, sort out authentication issues.
4201 logging.debug('ensuring credentials exist')
4202 for cl in changes:
4203 cl.EnsureAuthenticated(force=False, refresh=True)
4204
4205 def fetch(cl):
4206 try:
4207 return (cl, cl.GetStatus())
4208 except:
4209 # See http://crbug.com/629863.
4210 logging.exception('failed to fetch status for %s:', cl)
4211 raise
4212
4213 threads_count = len(changes)
4214 if max_processes:
4215 threads_count = max(1, min(threads_count, max_processes))
4216 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
4217
4218 pool = ThreadPool(threads_count)
4219 fetched_cls = set()
4220 try:
4221 it = pool.imap_unordered(fetch, changes).__iter__()
4222 while True:
4223 try:
4224 cl, status = it.next(timeout=5)
4225 except multiprocessing.TimeoutError:
4226 break
4227 fetched_cls.add(cl)
4228 yield cl, status
4229 finally:
4230 pool.close()
4231
4232 # Add any branches that failed to fetch.
4233 for cl in set(changes) - fetched_cls:
4234 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004235
rmistry@google.com2dd99862015-06-22 12:22:18 +00004236
4237def upload_branch_deps(cl, args):
4238 """Uploads CLs of local branches that are dependents of the current branch.
4239
4240 If the local branch dependency tree looks like:
4241 test1 -> test2.1 -> test3.1
4242 -> test3.2
4243 -> test2.2 -> test3.3
4244
4245 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
4246 run on the dependent branches in this order:
4247 test2.1, test3.1, test3.2, test2.2, test3.3
4248
4249 Note: This function does not rebase your local dependent branches. Use it when
4250 you make a change to the parent branch that will not conflict with its
4251 dependent branches, and you would like their dependencies updated in
4252 Rietveld.
4253 """
4254 if git_common.is_dirty_git_tree('upload-branch-deps'):
4255 return 1
4256
4257 root_branch = cl.GetBranch()
4258 if root_branch is None:
4259 DieWithError('Can\'t find dependent branches from detached HEAD state. '
4260 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01004261 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00004262 DieWithError('Current branch does not have an uploaded CL. We cannot set '
4263 'patchset dependencies without an uploaded CL.')
4264
4265 branches = RunGit(['for-each-ref',
4266 '--format=%(refname:short) %(upstream:short)',
4267 'refs/heads'])
4268 if not branches:
4269 print('No local branches found.')
4270 return 0
4271
4272 # Create a dictionary of all local branches to the branches that are dependent
4273 # on it.
4274 tracked_to_dependents = collections.defaultdict(list)
4275 for b in branches.splitlines():
4276 tokens = b.split()
4277 if len(tokens) == 2:
4278 branch_name, tracked = tokens
4279 tracked_to_dependents[tracked].append(branch_name)
4280
vapiera7fbd5a2016-06-16 09:17:49 -07004281 print()
4282 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004283 dependents = []
4284 def traverse_dependents_preorder(branch, padding=''):
4285 dependents_to_process = tracked_to_dependents.get(branch, [])
4286 padding += ' '
4287 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07004288 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00004289 dependents.append(dependent)
4290 traverse_dependents_preorder(dependent, padding)
4291 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07004292 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004293
4294 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004295 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004296 return 0
4297
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01004298 confirm_or_exit('This command will checkout all dependent branches and run '
4299 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004300
andybons@chromium.org962f9462016-02-03 20:00:42 +00004301 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00004302 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00004303 args.extend(['-t', 'Updated patchset dependency'])
4304
rmistry@google.com2dd99862015-06-22 12:22:18 +00004305 # Record all dependents that failed to upload.
4306 failures = {}
4307 # Go through all dependents, checkout the branch and upload.
4308 try:
4309 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004310 print()
4311 print('--------------------------------------')
4312 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004313 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07004314 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004315 try:
4316 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004317 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004318 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004319 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00004320 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07004321 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004322 finally:
4323 # Swap back to the original root branch.
4324 RunGit(['checkout', '-q', root_branch])
4325
vapiera7fbd5a2016-06-16 09:17:49 -07004326 print()
4327 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004328 for dependent_branch in dependents:
4329 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07004330 print(' %s : %s' % (dependent_branch, upload_status))
4331 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004332
4333 return 0
4334
4335
kmarshall3bff56b2016-06-06 18:31:47 -07004336def CMDarchive(parser, args):
4337 """Archives and deletes branches associated with closed changelists."""
4338 parser.add_option(
4339 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07004340 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07004341 parser.add_option(
4342 '-f', '--force', action='store_true',
4343 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07004344 parser.add_option(
4345 '-d', '--dry-run', action='store_true',
4346 help='Skip the branch tagging and removal steps.')
4347 parser.add_option(
4348 '-t', '--notags', action='store_true',
4349 help='Do not tag archived branches. '
4350 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004351
4352 auth.add_auth_options(parser)
4353 options, args = parser.parse_args(args)
4354 if args:
4355 parser.error('Unsupported args: %s' % ' '.join(args))
4356 auth_config = auth.extract_auth_config_from_options(options)
4357
4358 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4359 if not branches:
4360 return 0
4361
vapiera7fbd5a2016-06-16 09:17:49 -07004362 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004363 changes = [Changelist(branchref=b, auth_config=auth_config)
4364 for b in branches.splitlines()]
4365 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4366 statuses = get_cl_statuses(changes,
4367 fine_grained=True,
4368 max_processes=options.maxjobs)
4369 proposal = [(cl.GetBranch(),
4370 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4371 for cl, status in statuses
4372 if status == 'closed']
4373 proposal.sort()
4374
4375 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004376 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004377 return 0
4378
4379 current_branch = GetCurrentBranch()
4380
vapiera7fbd5a2016-06-16 09:17:49 -07004381 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004382 if options.notags:
4383 for next_item in proposal:
4384 print(' ' + next_item[0])
4385 else:
4386 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4387 for next_item in proposal:
4388 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004389
kmarshall9249e012016-08-23 12:02:16 -07004390 # Quit now on precondition failure or if instructed by the user, either
4391 # via an interactive prompt or by command line flags.
4392 if options.dry_run:
4393 print('\nNo changes were made (dry run).\n')
4394 return 0
4395 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004396 print('You are currently on a branch \'%s\' which is associated with a '
4397 'closed codereview issue, so archive cannot proceed. Please '
4398 'checkout another branch and run this command again.' %
4399 current_branch)
4400 return 1
kmarshall9249e012016-08-23 12:02:16 -07004401 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004402 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4403 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004404 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004405 return 1
4406
4407 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004408 if not options.notags:
4409 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004410 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004411
vapiera7fbd5a2016-06-16 09:17:49 -07004412 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004413
4414 return 0
4415
4416
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004417def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004418 """Show status of changelists.
4419
4420 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004421 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004422 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004423 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004424 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004425 - Magenta in the commit queue
4426 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004427 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004428
4429 Also see 'git cl comments'.
4430 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004431 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004432 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004433 parser.add_option('-f', '--fast', action='store_true',
4434 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004435 parser.add_option(
4436 '-j', '--maxjobs', action='store', type=int,
4437 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004438
4439 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004440 _add_codereview_issue_select_options(
4441 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004442 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004443 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004444 if args:
4445 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004446 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004447
iannuccie53c9352016-08-17 14:40:40 -07004448 if options.issue is not None and not options.field:
4449 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004450
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004451 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004452 cl = Changelist(auth_config=auth_config, issue=options.issue,
4453 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004454 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004455 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004456 elif options.field == 'id':
4457 issueid = cl.GetIssue()
4458 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004459 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004460 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004461 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004462 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004463 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004464 elif options.field == 'status':
4465 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004466 elif options.field == 'url':
4467 url = cl.GetIssueURL()
4468 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004469 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004470 return 0
4471
4472 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4473 if not branches:
4474 print('No local branch found.')
4475 return 0
4476
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004477 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004478 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004479 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004480 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004481 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004482 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004483 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004484
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004485 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004486 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
4487 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4488 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004489 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004490 c, status = output.next()
4491 branch_statuses[c.GetBranch()] = status
4492 status = branch_statuses.pop(branch)
4493 url = cl.GetIssueURL()
4494 if url and (not status or status == 'error'):
4495 # The issue probably doesn't exist anymore.
4496 url += ' (broken)'
4497
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004498 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004499 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004500 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004501 color = ''
4502 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004503 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07004504 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004505 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07004506 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004507
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004508
4509 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07004510 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004511 print('Current branch: %s' % branch)
4512 for cl in changes:
4513 if cl.GetBranch() == branch:
4514 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004515 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004516 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004517 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004518 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004519 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004520 print('Issue description:')
4521 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004522 return 0
4523
4524
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004525def colorize_CMDstatus_doc():
4526 """To be called once in main() to add colors to git cl status help."""
4527 colors = [i for i in dir(Fore) if i[0].isupper()]
4528
4529 def colorize_line(line):
4530 for color in colors:
4531 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004532 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004533 indent = len(line) - len(line.lstrip(' ')) + 1
4534 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4535 return line
4536
4537 lines = CMDstatus.__doc__.splitlines()
4538 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4539
4540
phajdan.jre328cf92016-08-22 04:12:17 -07004541def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004542 if path == '-':
4543 json.dump(contents, sys.stdout)
4544 else:
4545 with open(path, 'w') as f:
4546 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004547
4548
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004549@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004550def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004551 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004552
4553 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004554 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004555 parser.add_option('-r', '--reverse', action='store_true',
4556 help='Lookup the branch(es) for the specified issues. If '
4557 'no issues are specified, all branches with mapped '
4558 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004559 parser.add_option('--json',
4560 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004561 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004562 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004563 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004564
dnj@chromium.org406c4402015-03-03 17:22:28 +00004565 if options.reverse:
4566 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004567 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004568 # Reverse issue lookup.
4569 issue_branch_map = {}
4570 for branch in branches:
4571 cl = Changelist(branchref=branch)
4572 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
4573 if not args:
4574 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004575 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004576 for issue in args:
4577 if not issue:
4578 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004579 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004580 print('Branch for issue number %s: %s' % (
4581 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004582 if options.json:
4583 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004584 return 0
4585
4586 if len(args) > 0:
4587 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4588 if not issue.valid:
4589 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4590 'or no argument to list it.\n'
4591 'Maybe you want to run git cl status?')
4592 cl = Changelist(codereview=issue.codereview)
4593 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004594 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004595 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004596 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4597 if options.json:
4598 write_json(options.json, {
4599 'issue': cl.GetIssue(),
4600 'issue_url': cl.GetIssueURL(),
4601 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004602 return 0
4603
4604
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004605def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004606 """Shows or posts review comments for any changelist."""
4607 parser.add_option('-a', '--add-comment', dest='comment',
4608 help='comment to add to an issue')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004609 parser.add_option('-i', '--issue', dest='issue',
4610 help='review issue id (defaults to current issue). '
4611 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004612 parser.add_option('-m', '--machine-readable', dest='readable',
4613 action='store_false', default=True,
4614 help='output comments in a format compatible with '
4615 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004616 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004617 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004618 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004619 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004620 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004621 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004622 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004623
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004624 issue = None
4625 if options.issue:
4626 try:
4627 issue = int(options.issue)
4628 except ValueError:
4629 DieWithError('A review issue id is expected to be a number')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004630 if not options.forced_codereview:
4631 parser.error('--gerrit or --rietveld is required if --issue is specified')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004632
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004633 cl = Changelist(issue=issue,
Andrii Shyshkalov70909e12017-04-10 14:38:32 +02004634 codereview=options.forced_codereview,
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004635 auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004636
4637 if options.comment:
4638 cl.AddComment(options.comment)
4639 return 0
4640
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004641 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4642 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004643 for comment in summary:
4644 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004645 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004646 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004647 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004648 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004649 color = Fore.MAGENTA
4650 else:
4651 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004652 print('\n%s%s %s%s\n%s' % (
4653 color,
4654 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4655 comment.sender,
4656 Fore.RESET,
4657 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4658
smut@google.comc85ac942015-09-15 16:34:43 +00004659 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004660 def pre_serialize(c):
4661 dct = c.__dict__.copy()
4662 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4663 return dct
smut@google.comc85ac942015-09-15 16:34:43 +00004664 with open(options.json_file, 'wb') as f:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004665 json.dump(map(pre_serialize, summary), f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004666 return 0
4667
4668
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004669@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004670def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004671 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004672 parser.add_option('-d', '--display', action='store_true',
4673 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004674 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004675 help='New description to set for this issue (- for stdin, '
4676 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004677 parser.add_option('-f', '--force', action='store_true',
4678 help='Delete any unpublished Gerrit edits for this issue '
4679 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004680
4681 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004682 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004683 options, args = parser.parse_args(args)
4684 _process_codereview_select_options(parser, options)
4685
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004686 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004687 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004688 target_issue_arg = ParseIssueNumberArgument(args[0],
4689 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004690 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004691 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004692
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004693 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004694
martiniss6eda05f2016-06-30 10:18:35 -07004695 kwargs = {
4696 'auth_config': auth_config,
4697 'codereview': options.forced_codereview,
4698 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004699 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004700 if target_issue_arg:
4701 kwargs['issue'] = target_issue_arg.issue
4702 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004703 if target_issue_arg.codereview and not options.forced_codereview:
4704 detected_codereview_from_url = True
4705 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004706
4707 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004708 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004709 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004710 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004711
4712 if detected_codereview_from_url:
4713 logging.info('canonical issue/change URL: %s (type: %s)\n',
4714 cl.GetIssueURL(), target_issue_arg.codereview)
4715
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004716 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004717
smut@google.com34fb6b12015-07-13 20:03:26 +00004718 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004719 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004720 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004721
4722 if options.new_description:
4723 text = options.new_description
4724 if text == '-':
4725 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004726 elif text == '+':
4727 base_branch = cl.GetCommonAncestorWithUpstream()
4728 change = cl.GetChange(base_branch, None, local_description=True)
4729 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004730
4731 description.set_description(text)
4732 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004733 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004734
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004735 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004736 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004737 return 0
4738
4739
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004740def CreateDescriptionFromLog(args):
4741 """Pulls out the commit log to use as a base for the CL description."""
4742 log_args = []
4743 if len(args) == 1 and not args[0].endswith('.'):
4744 log_args = [args[0] + '..']
4745 elif len(args) == 1 and args[0].endswith('...'):
4746 log_args = [args[0][:-1]]
4747 elif len(args) == 2:
4748 log_args = [args[0] + '..' + args[1]]
4749 else:
4750 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004751 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004752
4753
thestig@chromium.org44202a22014-03-11 19:22:18 +00004754def CMDlint(parser, args):
4755 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004756 parser.add_option('--filter', action='append', metavar='-x,+y',
4757 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004758 auth.add_auth_options(parser)
4759 options, args = parser.parse_args(args)
4760 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004761
4762 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004763 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004764 try:
4765 import cpplint
4766 import cpplint_chromium
4767 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004768 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004769 return 1
4770
4771 # Change the current working directory before calling lint so that it
4772 # shows the correct base.
4773 previous_cwd = os.getcwd()
4774 os.chdir(settings.GetRoot())
4775 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004776 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004777 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4778 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004779 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004780 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004781 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004782
4783 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004784 command = args + files
4785 if options.filter:
4786 command = ['--filter=' + ','.join(options.filter)] + command
4787 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004788
4789 white_regex = re.compile(settings.GetLintRegex())
4790 black_regex = re.compile(settings.GetLintIgnoreRegex())
4791 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4792 for filename in filenames:
4793 if white_regex.match(filename):
4794 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004795 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004796 else:
4797 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4798 extra_check_functions)
4799 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004800 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004801 finally:
4802 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004803 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004804 if cpplint._cpplint_state.error_count != 0:
4805 return 1
4806 return 0
4807
4808
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004809def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004810 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004811 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004812 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004813 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004814 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004815 parser.add_option('--all', action='store_true',
4816 help='Run checks against all files, not just modified ones')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004817 auth.add_auth_options(parser)
4818 options, args = parser.parse_args(args)
4819 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004820
sbc@chromium.org71437c02015-04-09 19:29:40 +00004821 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004822 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004823 return 1
4824
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004825 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004826 if args:
4827 base_branch = args[0]
4828 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004829 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004830 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004831
Aaron Gable8076c282017-11-29 14:39:41 -08004832 if options.all:
4833 base_change = cl.GetChange(base_branch, None)
4834 files = [('M', f) for f in base_change.AllFiles()]
4835 change = presubmit_support.GitChange(
4836 base_change.Name(),
4837 base_change.FullDescriptionText(),
4838 base_change.RepositoryRoot(),
4839 files,
4840 base_change.issue,
4841 base_change.patchset,
4842 base_change.author_email,
4843 base_change._upstream)
4844 else:
4845 change = cl.GetChange(base_branch, None)
4846
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004847 cl.RunHook(
4848 committing=not options.upload,
4849 may_prompt=False,
4850 verbose=options.verbose,
Aaron Gable8076c282017-11-29 14:39:41 -08004851 change=change)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004852 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004853
4854
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004855def GenerateGerritChangeId(message):
4856 """Returns Ixxxxxx...xxx change id.
4857
4858 Works the same way as
4859 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4860 but can be called on demand on all platforms.
4861
4862 The basic idea is to generate git hash of a state of the tree, original commit
4863 message, author/committer info and timestamps.
4864 """
4865 lines = []
4866 tree_hash = RunGitSilent(['write-tree'])
4867 lines.append('tree %s' % tree_hash.strip())
4868 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4869 if code == 0:
4870 lines.append('parent %s' % parent.strip())
4871 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4872 lines.append('author %s' % author.strip())
4873 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4874 lines.append('committer %s' % committer.strip())
4875 lines.append('')
4876 # Note: Gerrit's commit-hook actually cleans message of some lines and
4877 # whitespace. This code is not doing this, but it clearly won't decrease
4878 # entropy.
4879 lines.append(message)
4880 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4881 stdin='\n'.join(lines))
4882 return 'I%s' % change_hash.strip()
4883
4884
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004885def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004886 """Computes the remote branch ref to use for the CL.
4887
4888 Args:
4889 remote (str): The git remote for the CL.
4890 remote_branch (str): The git remote branch for the CL.
4891 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004892 """
4893 if not (remote and remote_branch):
4894 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004895
wittman@chromium.org455dc922015-01-26 20:15:50 +00004896 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004897 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004898 # refs, which are then translated into the remote full symbolic refs
4899 # below.
4900 if '/' not in target_branch:
4901 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4902 else:
4903 prefix_replacements = (
4904 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4905 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4906 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4907 )
4908 match = None
4909 for regex, replacement in prefix_replacements:
4910 match = re.search(regex, target_branch)
4911 if match:
4912 remote_branch = target_branch.replace(match.group(0), replacement)
4913 break
4914 if not match:
4915 # This is a branch path but not one we recognize; use as-is.
4916 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004917 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4918 # Handle the refs that need to land in different refs.
4919 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004920
wittman@chromium.org455dc922015-01-26 20:15:50 +00004921 # Create the true path to the remote branch.
4922 # Does the following translation:
4923 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4924 # * refs/remotes/origin/master -> refs/heads/master
4925 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4926 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4927 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4928 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4929 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4930 'refs/heads/')
4931 elif remote_branch.startswith('refs/remotes/branch-heads'):
4932 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004933
wittman@chromium.org455dc922015-01-26 20:15:50 +00004934 return remote_branch
4935
4936
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004937def cleanup_list(l):
4938 """Fixes a list so that comma separated items are put as individual items.
4939
4940 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4941 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4942 """
4943 items = sum((i.split(',') for i in l), [])
4944 stripped_items = (i.strip() for i in items)
4945 return sorted(filter(None, stripped_items))
4946
4947
Aaron Gable4db38df2017-11-03 14:59:07 -07004948@subcommand.usage('[flags]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004949def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004950 """Uploads the current changelist to codereview.
4951
4952 Can skip dependency patchset uploads for a branch by running:
4953 git config branch.branch_name.skip-deps-uploads True
4954 To unset run:
4955 git config --unset branch.branch_name.skip-deps-uploads
4956 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004957
4958 If the name of the checked out branch starts with "bug-" or "fix-" followed by
4959 a bug number, this bug number is automatically populated in the CL
4960 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004961
4962 If subject contains text in square brackets or has "<text>: " prefix, such
4963 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
4964 [git-cl] add support for hashtags
4965 Foo bar: implement foo
4966 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004967 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004968 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4969 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004970 parser.add_option('--bypass-watchlists', action='store_true',
4971 dest='bypass_watchlists',
4972 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004973 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004974 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004975 parser.add_option('--message', '-m', dest='message',
4976 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004977 parser.add_option('-b', '--bug',
4978 help='pre-populate the bug number(s) for this issue. '
4979 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004980 parser.add_option('--message-file', dest='message_file',
4981 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004982 parser.add_option('--title', '-t', dest='title',
4983 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004984 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004985 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004986 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004987 parser.add_option('--tbrs',
4988 action='append', default=[],
4989 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004990 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004991 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004992 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004993 parser.add_option('--hashtag', dest='hashtags',
4994 action='append', default=[],
4995 help=('Gerrit hashtag for new CL; '
4996 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004997 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004998 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004999 parser.add_option('--emulate_svn_auto_props',
5000 '--emulate-svn-auto-props',
5001 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00005002 dest="emulate_svn_auto_props",
5003 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00005004 parser.add_option('-c', '--use-commit-queue', action='store_true',
Aaron Gableedbc4132017-09-11 13:22:28 -07005005 help='tell the commit queue to commit this patchset; '
5006 'implies --send-mail')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00005007 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00005008 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00005009 metavar='TARGET',
5010 help='Apply CL to remote ref TARGET. ' +
5011 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00005012 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005013 help='Squash multiple commits into one')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00005014 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005015 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07005016 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005017 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07005018 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
5019 const='TBR', help='add a set of OWNERS to TBR')
5020 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
5021 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00005022 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
5023 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00005024 help='Send the patchset to do a CQ dry run right after '
5025 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00005026 parser.add_option('--dependencies', action='store_true',
5027 help='Uploads CLs of all the local branches that depend on '
5028 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00005029
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005030 # TODO: remove Rietveld flags
5031 parser.add_option('--private', action='store_true',
5032 help='set the review private (rietveld only)')
5033 parser.add_option('--email', default=None,
5034 help='email address to use to connect to Rietveld')
5035
rmistry@google.com2dd99862015-06-22 12:22:18 +00005036 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00005037 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005038 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005039 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005040 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005041 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005042 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005043
sbc@chromium.org71437c02015-04-09 19:29:40 +00005044 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00005045 return 1
5046
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005047 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07005048 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005049 options.cc = cleanup_list(options.cc)
5050
tandriib80458a2016-06-23 12:20:07 -07005051 if options.message_file:
5052 if options.message:
5053 parser.error('only one of --message and --message-file allowed.')
5054 options.message = gclient_utils.FileRead(options.message_file)
5055 options.message_file = None
5056
tandrii4d0545a2016-07-06 03:56:49 -07005057 if options.cq_dry_run and options.use_commit_queue:
5058 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
5059
Aaron Gableedbc4132017-09-11 13:22:28 -07005060 if options.use_commit_queue:
5061 options.send_mail = True
5062
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00005063 # For sanity of test expectations, do this otherwise lazy-loading *now*.
5064 settings.GetIsGerrit()
5065
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005066 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00005067 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005068
5069
Francois Dorayd42c6812017-05-30 15:10:20 -04005070@subcommand.usage('--description=<description file>')
5071def CMDsplit(parser, args):
5072 """Splits a branch into smaller branches and uploads CLs.
5073
5074 Creates a branch and uploads a CL for each group of files modified in the
5075 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07005076 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04005077 the shared OWNERS file.
5078 """
5079 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05005080 help="A text file containing a CL description in which "
5081 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005082 parser.add_option("-c", "--comment", dest="comment_file",
5083 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11005084 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
5085 default=False,
5086 help="List the files and reviewers for each CL that would "
5087 "be created, but don't create branches or CLs.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005088 options, _ = parser.parse_args(args)
5089
5090 if not options.description_file:
5091 parser.error('No --description flag specified.')
5092
5093 def WrappedCMDupload(args):
5094 return CMDupload(OptionParser(), args)
5095
5096 return split_cl.SplitCl(options.description_file, options.comment_file,
Chris Watkinsba28e462017-12-13 11:22:17 +11005097 Changelist, WrappedCMDupload, options.dry_run)
Francois Dorayd42c6812017-05-30 15:10:20 -04005098
5099
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005100@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005101def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005102 """DEPRECATED: Used to commit the current changelist via git-svn."""
5103 message = ('git-cl no longer supports committing to SVN repositories via '
5104 'git-svn. You probably want to use `git cl land` instead.')
5105 print(message)
5106 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005107
5108
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005109# Two special branches used by git cl land.
5110MERGE_BRANCH = 'git-cl-commit'
5111CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
5112
5113
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005114@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00005115def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005116 """Commits the current changelist via git.
5117
5118 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
5119 upstream and closes the issue automatically and atomically.
5120
5121 Otherwise (in case of Rietveld):
5122 Squashes branch into a single commit.
5123 Updates commit message with metadata (e.g. pointer to review).
5124 Pushes the code upstream.
5125 Updates review and closes.
5126 """
5127 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
5128 help='bypass upload presubmit hook')
5129 parser.add_option('-m', dest='message',
5130 help="override review description")
Aaron Gablef7543cd2017-07-20 14:26:31 -07005131 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005132 help="force yes to questions (don't prompt)")
5133 parser.add_option('-c', dest='contributor',
5134 help="external contributor for patch (appended to " +
5135 "description and used as author for git). Should be " +
5136 "formatted as 'First Last <email@example.com>'")
5137 add_git_similarity(parser)
5138 auth.add_auth_options(parser)
5139 (options, args) = parser.parse_args(args)
5140 auth_config = auth.extract_auth_config_from_options(options)
5141
5142 cl = Changelist(auth_config=auth_config)
5143
5144 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
5145 if cl.IsGerrit():
5146 if options.message:
5147 # This could be implemented, but it requires sending a new patch to
5148 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
5149 # Besides, Gerrit has the ability to change the commit message on submit
5150 # automatically, thus there is no need to support this option (so far?).
5151 parser.error('-m MESSAGE option is not supported for Gerrit.')
5152 if options.contributor:
5153 parser.error(
5154 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
5155 'Before uploading a commit to Gerrit, ensure it\'s author field is '
5156 'the contributor\'s "name <email>". If you can\'t upload such a '
5157 'commit for review, contact your repository admin and request'
5158 '"Forge-Author" permission.')
5159 if not cl.GetIssue():
5160 DieWithError('You must upload the change first to Gerrit.\n'
5161 ' If you would rather have `git cl land` upload '
5162 'automatically for you, see http://crbug.com/642759')
5163 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
5164 options.verbose)
5165
5166 current = cl.GetBranch()
5167 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
5168 if remote == '.':
5169 print()
5170 print('Attempting to push branch %r into another local branch!' % current)
5171 print()
5172 print('Either reparent this branch on top of origin/master:')
5173 print(' git reparent-branch --root')
5174 print()
5175 print('OR run `git rebase-update` if you think the parent branch is ')
5176 print('already committed.')
5177 print()
5178 print(' Current parent: %r' % upstream_branch)
5179 return 1
5180
5181 if not args:
5182 # Default to merging against our best guess of the upstream branch.
5183 args = [cl.GetUpstreamBranch()]
5184
5185 if options.contributor:
5186 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07005187 print("Please provide contributor as 'First Last <email@example.com>'")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005188 return 1
5189
5190 base_branch = args[0]
5191
5192 if git_common.is_dirty_git_tree('land'):
5193 return 1
5194
5195 # This rev-list syntax means "show all commits not in my branch that
5196 # are in base_branch".
5197 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
5198 base_branch]).splitlines()
5199 if upstream_commits:
5200 print('Base branch "%s" has %d commits '
5201 'not in this branch.' % (base_branch, len(upstream_commits)))
5202 print('Run "git merge %s" before attempting to land.' % base_branch)
5203 return 1
5204
5205 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
5206 if not options.bypass_hooks:
5207 author = None
5208 if options.contributor:
5209 author = re.search(r'\<(.*)\>', options.contributor).group(1)
5210 hook_results = cl.RunHook(
5211 committing=True,
5212 may_prompt=not options.force,
5213 verbose=options.verbose,
5214 change=cl.GetChange(merge_base, author))
5215 if not hook_results.should_continue():
5216 return 1
5217
5218 # Check the tree status if the tree status URL is set.
5219 status = GetTreeStatus()
5220 if 'closed' == status:
5221 print('The tree is closed. Please wait for it to reopen. Use '
5222 '"git cl land --bypass-hooks" to commit on a closed tree.')
5223 return 1
5224 elif 'unknown' == status:
5225 print('Unable to determine tree status. Please verify manually and '
5226 'use "git cl land --bypass-hooks" to commit on a closed tree.')
5227 return 1
5228
5229 change_desc = ChangeDescription(options.message)
5230 if not change_desc.description and cl.GetIssue():
5231 change_desc = ChangeDescription(cl.GetDescription())
5232
5233 if not change_desc.description:
5234 if not cl.GetIssue() and options.bypass_hooks:
5235 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
5236 else:
5237 print('No description set.')
5238 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
5239 return 1
5240
5241 # Keep a separate copy for the commit message, because the commit message
5242 # contains the link to the Rietveld issue, while the Rietveld message contains
5243 # the commit viewvc url.
5244 if cl.GetIssue():
Aaron Gablea1bab272017-04-11 16:38:18 -07005245 change_desc.update_reviewers(
Robert Iannucci6c98dc62017-04-18 11:38:00 -07005246 get_approving_reviewers(cl.GetIssueProperties()), [])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005247
5248 commit_desc = ChangeDescription(change_desc.description)
5249 if cl.GetIssue():
5250 # Xcode won't linkify this URL unless there is a non-whitespace character
5251 # after it. Add a period on a new line to circumvent this. Also add a space
5252 # before the period to make sure that Gitiles continues to correctly resolve
5253 # the URL.
5254 commit_desc.append_footer('Review-Url: %s .' % cl.GetIssueURL())
5255 if options.contributor:
5256 commit_desc.append_footer('Patch from %s.' % options.contributor)
5257
5258 print('Description:')
5259 print(commit_desc.description)
5260
5261 branches = [merge_base, cl.GetBranchRef()]
5262 if not options.force:
5263 print_stats(options.similarity, options.find_copies, branches)
5264
5265 # We want to squash all this branch's commits into one commit with the proper
5266 # description. We do this by doing a "reset --soft" to the base branch (which
5267 # keeps the working copy the same), then landing that.
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005268 # Delete the special branches if they exist.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005269 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
5270 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
5271 result = RunGitWithCode(showref_cmd)
5272 if result[0] == 0:
5273 RunGit(['branch', '-D', branch])
5274
5275 # We might be in a directory that's present in this branch but not in the
5276 # trunk. Move up to the top of the tree so that git commands that expect a
5277 # valid CWD won't fail after we check out the merge branch.
5278 rel_base_path = settings.GetRelativeRoot()
5279 if rel_base_path:
5280 os.chdir(rel_base_path)
5281
5282 # Stuff our change into the merge branch.
5283 # We wrap in a try...finally block so if anything goes wrong,
5284 # we clean up the branches.
5285 retcode = -1
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005286 revision = None
5287 try:
5288 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
5289 RunGit(['reset', '--soft', merge_base])
5290 if options.contributor:
5291 RunGit(
5292 [
5293 'commit', '--author', options.contributor,
5294 '-m', commit_desc.description,
5295 ])
5296 else:
5297 RunGit(['commit', '-m', commit_desc.description])
5298
5299 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
5300 mirror = settings.GetGitMirror(remote)
5301 if mirror:
5302 pushurl = mirror.url
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005303 git_numberer_enabled = _is_git_numberer_enabled(pushurl, branch)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005304 else:
5305 pushurl = remote # Usually, this is 'origin'.
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005306 git_numberer_enabled = _is_git_numberer_enabled(
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005307 RunGit(['config', 'remote.%s.url' % remote]).strip(), branch)
5308
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005309 retcode = PushToGitWithAutoRebase(
5310 pushurl, branch, commit_desc.description, git_numberer_enabled)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005311 if retcode == 0:
5312 revision = RunGit(['rev-parse', 'HEAD']).strip()
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005313 if git_numberer_enabled:
5314 change_desc = ChangeDescription(
5315 RunGit(['show', '-s', '--format=%B', 'HEAD']).strip())
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005316 except: # pylint: disable=bare-except
5317 if _IS_BEING_TESTED:
5318 logging.exception('this is likely your ACTUAL cause of test failure.\n'
5319 + '-' * 30 + '8<' + '-' * 30)
5320 logging.error('\n' + '-' * 30 + '8<' + '-' * 30 + '\n\n\n')
5321 raise
5322 finally:
5323 # And then swap back to the original branch and clean up.
5324 RunGit(['checkout', '-q', cl.GetBranch()])
5325 RunGit(['branch', '-D', MERGE_BRANCH])
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005326 RunGit(['branch', '-D', CHERRY_PICK_BRANCH], error_ok=True)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005327
5328 if not revision:
5329 print('Failed to push. If this persists, please file a bug.')
5330 return 1
5331
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005332 if cl.GetIssue():
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005333 viewvc_url = settings.GetViewVCUrl()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005334 if viewvc_url and revision:
5335 change_desc.append_footer(
5336 'Committed: %s%s' % (viewvc_url, revision))
5337 elif revision:
5338 change_desc.append_footer('Committed: %s' % (revision,))
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005339 print('Closing issue '
5340 '(you may be prompted for your codereview password)...')
5341 cl.UpdateDescription(change_desc.description)
5342 cl.CloseIssue()
5343 props = cl.GetIssueProperties()
5344 patch_num = len(props['patchsets'])
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005345 comment = "Committed patchset #%d (id:%d) manually as %s" % (
5346 patch_num, props['patchsets'][-1], revision)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005347 if options.bypass_hooks:
5348 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
5349 else:
5350 comment += ' (presubmit successful).'
5351 cl.RpcServer().add_comment(cl.GetIssue(), comment)
5352
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005353 if os.path.isfile(POSTUPSTREAM_HOOK):
5354 RunCommand([POSTUPSTREAM_HOOK, merge_base], error_ok=True)
5355
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005356 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005357
5358
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005359def PushToGitWithAutoRebase(remote, branch, original_description,
5360 git_numberer_enabled, max_attempts=3):
5361 """Pushes current HEAD commit on top of remote's branch.
5362
5363 Attempts to fetch and autorebase on push failures.
5364 Adds git number footers on the fly.
5365
5366 Returns integer code from last command.
5367 """
5368 cherry = RunGit(['rev-parse', 'HEAD']).strip()
5369 code = 0
5370 attempts_left = max_attempts
5371 while attempts_left:
5372 attempts_left -= 1
5373 print('Attempt %d of %d' % (max_attempts - attempts_left, max_attempts))
5374
5375 # Fetch remote/branch into local cherry_pick_branch, overriding the latter.
5376 # If fetch fails, retry.
5377 print('Fetching %s/%s...' % (remote, branch))
5378 code, out = RunGitWithCode(
5379 ['retry', 'fetch', remote,
5380 '+%s:refs/heads/%s' % (branch, CHERRY_PICK_BRANCH)])
5381 if code:
5382 print('Fetch failed with exit code %d.' % code)
5383 print(out.strip())
5384 continue
5385
5386 print('Cherry-picking commit on top of latest %s' % branch)
5387 RunGitWithCode(['checkout', 'refs/heads/%s' % CHERRY_PICK_BRANCH],
5388 suppress_stderr=True)
5389 parent_hash = RunGit(['rev-parse', 'HEAD']).strip()
5390 code, out = RunGitWithCode(['cherry-pick', cherry])
5391 if code:
5392 print('Your patch doesn\'t apply cleanly to \'%s\' HEAD @ %s, '
5393 'the following files have merge conflicts:' %
5394 (branch, parent_hash))
Aaron Gable7817f022017-12-12 09:43:17 -08005395 print(RunGit(['-c', 'core.quotePath=false', 'diff',
5396 '--name-status', '--diff-filter=U']).strip())
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005397 print('Please rebase your patch and try again.')
5398 RunGitWithCode(['cherry-pick', '--abort'])
5399 break
5400
5401 commit_desc = ChangeDescription(original_description)
5402 if git_numberer_enabled:
5403 logging.debug('Adding git number footers')
5404 parent_msg = RunGit(['show', '-s', '--format=%B', parent_hash]).strip()
5405 commit_desc.update_with_git_number_footers(parent_hash, parent_msg,
5406 branch)
5407 # Ensure timestamps are monotonically increasing.
5408 timestamp = max(1 + _get_committer_timestamp(parent_hash),
5409 _get_committer_timestamp('HEAD'))
5410 _git_amend_head(commit_desc.description, timestamp)
5411
5412 code, out = RunGitWithCode(
5413 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
5414 print(out)
5415 if code == 0:
5416 break
5417 if IsFatalPushFailure(out):
5418 print('Fatal push error. Make sure your .netrc credentials and git '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02005419 'user.email are correct and you have push access to the repo.\n'
5420 'Hint: run command below to diangose common Git/Gerrit credential '
5421 'problems:\n'
5422 ' git cl creds-check\n')
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005423 break
5424 return code
5425
5426
5427def IsFatalPushFailure(push_stdout):
5428 """True if retrying push won't help."""
5429 return '(prohibited by Gerrit)' in push_stdout
5430
5431
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005432@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005433def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00005434 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005435 parser.add_option('-b', dest='newbranch',
5436 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005437 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07005438 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005439 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07005440 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005441 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005442 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00005443 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005444 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005445 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005446 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005447
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005448
5449 group = optparse.OptionGroup(
5450 parser,
5451 'Options for continuing work on the current issue uploaded from a '
5452 'different clone (e.g. different machine). Must be used independently '
5453 'from the other options. No issue number should be specified, and the '
5454 'branch must have an issue number associated with it')
5455 group.add_option('--reapply', action='store_true', dest='reapply',
5456 help='Reset the branch and reapply the issue.\n'
5457 'CAUTION: This will undo any local changes in this '
5458 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005459
5460 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005461 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005462 parser.add_option_group(group)
5463
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005464 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005465 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005466 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005467 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005468 auth_config = auth.extract_auth_config_from_options(options)
5469
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005470 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005471 if options.newbranch:
5472 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005473 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005474 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005475
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005476 cl = Changelist(auth_config=auth_config,
5477 codereview=options.forced_codereview)
5478 if not cl.GetIssue():
5479 parser.error('current branch must have an associated issue')
5480
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005481 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005482 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005483 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005484
5485 RunGit(['reset', '--hard', upstream])
5486 if options.pull:
5487 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005488
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005489 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
5490 options.directory)
5491
5492 if len(args) != 1 or not args[0]:
5493 parser.error('Must specify issue number or url')
5494
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005495 target_issue_arg = ParseIssueNumberArgument(args[0],
5496 options.forced_codereview)
5497 if not target_issue_arg.valid:
5498 parser.error('invalid codereview url or CL id')
5499
5500 cl_kwargs = {
5501 'auth_config': auth_config,
5502 'codereview_host': target_issue_arg.hostname,
5503 'codereview': options.forced_codereview,
5504 }
5505 detected_codereview_from_url = False
5506 if target_issue_arg.codereview and not options.forced_codereview:
5507 detected_codereview_from_url = True
5508 cl_kwargs['codereview'] = target_issue_arg.codereview
5509 cl_kwargs['issue'] = target_issue_arg.issue
5510
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005511 # We don't want uncommitted changes mixed up with the patch.
5512 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005513 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005514
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005515 if options.newbranch:
5516 if options.force:
5517 RunGit(['branch', '-D', options.newbranch],
5518 stderr=subprocess2.PIPE, error_ok=True)
5519 RunGit(['new-branch', options.newbranch])
5520
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005521 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005522
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005523 if cl.IsGerrit():
5524 if options.reject:
5525 parser.error('--reject is not supported with Gerrit codereview.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005526 if options.directory:
5527 parser.error('--directory is not supported with Gerrit codereview.')
5528
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005529 if detected_codereview_from_url:
5530 print('canonical issue/change URL: %s (type: %s)\n' %
5531 (cl.GetIssueURL(), target_issue_arg.codereview))
5532
5533 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07005534 options.nocommit, options.directory,
5535 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005536
5537
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005538def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005539 """Fetches the tree status and returns either 'open', 'closed',
5540 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005541 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005542 if url:
5543 status = urllib2.urlopen(url).read().lower()
5544 if status.find('closed') != -1 or status == '0':
5545 return 'closed'
5546 elif status.find('open') != -1 or status == '1':
5547 return 'open'
5548 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005549 return 'unset'
5550
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005551
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005552def GetTreeStatusReason():
5553 """Fetches the tree status from a json url and returns the message
5554 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005555 url = settings.GetTreeStatusUrl()
5556 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005557 connection = urllib2.urlopen(json_url)
5558 status = json.loads(connection.read())
5559 connection.close()
5560 return status['message']
5561
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005562
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005563def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005564 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005565 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005566 status = GetTreeStatus()
5567 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005568 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005569 return 2
5570
vapiera7fbd5a2016-06-16 09:17:49 -07005571 print('The tree is %s' % status)
5572 print()
5573 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005574 if status != 'open':
5575 return 1
5576 return 0
5577
5578
maruel@chromium.org15192402012-09-06 12:38:29 +00005579def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005580 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005581 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005582 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005583 '-b', '--bot', action='append',
5584 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5585 'times to specify multiple builders. ex: '
5586 '"-b win_rel -b win_layout". See '
5587 'the try server waterfall for the builders name and the tests '
5588 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005589 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005590 '-B', '--bucket', default='',
5591 help=('Buildbucket bucket to send the try requests.'))
5592 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005593 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005594 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005595 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005596 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005597 help='Revision to use for the try job; default: the revision will '
5598 'be determined by the try recipe that builder runs, which usually '
5599 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005600 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005601 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005602 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005603 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005604 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005605 '--project',
5606 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005607 'in recipe to determine to which repository or directory to '
5608 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005609 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005610 '-p', '--property', dest='properties', action='append', default=[],
5611 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005612 'key2=value2 etc. The value will be treated as '
5613 'json if decodable, or as string otherwise. '
5614 'NOTE: using this may make your try job not usable for CQ, '
5615 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005616 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005617 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5618 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005619 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005620 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09005621 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005622 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09005623 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005624 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005625
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005626 if options.master and options.master.startswith('luci.'):
5627 parser.error(
5628 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005629 # Make sure that all properties are prop=value pairs.
5630 bad_params = [x for x in options.properties if '=' not in x]
5631 if bad_params:
5632 parser.error('Got properties with missing "=": %s' % bad_params)
5633
maruel@chromium.org15192402012-09-06 12:38:29 +00005634 if args:
5635 parser.error('Unknown arguments: %s' % args)
5636
Koji Ishii31c14782018-01-08 17:17:33 +09005637 cl = Changelist(auth_config=auth_config, issue=options.issue,
5638 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00005639 if not cl.GetIssue():
5640 parser.error('Need to upload first')
5641
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005642 if cl.IsGerrit():
5643 # HACK: warm up Gerrit change detail cache to save on RPCs.
5644 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5645
tandriie113dfd2016-10-11 10:20:12 -07005646 error_message = cl.CannotTriggerTryJobReason()
5647 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005648 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005649
borenet6c0efe62016-10-19 08:13:29 -07005650 if options.bucket and options.master:
5651 parser.error('Only one of --bucket and --master may be used.')
5652
qyearsley1fdfcb62016-10-24 13:22:03 -07005653 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005654
qyearsleydd49f942016-10-28 11:57:22 -07005655 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5656 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005657 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005658 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005659 print('git cl try with no bots now defaults to CQ dry run.')
5660 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5661 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005662
borenet6c0efe62016-10-19 08:13:29 -07005663 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005664 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005665 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005666 'of bot requires an initial job from a parent (usually a builder). '
5667 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005668 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005669 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005670
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005671 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005672 # TODO(tandrii): Checking local patchset against remote patchset is only
5673 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5674 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07005675 print('Warning: Codereview server has newer patchsets (%s) than most '
5676 'recent upload from local checkout (%s). Did a previous upload '
5677 'fail?\n'
5678 'By default, git cl try uses the latest patchset from '
5679 'codereview, continuing to use patchset %s.\n' %
5680 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07005681
tandrii568043b2016-10-11 07:49:18 -07005682 try:
borenet6c0efe62016-10-19 08:13:29 -07005683 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
5684 patchset)
tandrii568043b2016-10-11 07:49:18 -07005685 except BuildbucketResponseException as ex:
5686 print('ERROR: %s' % ex)
5687 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005688 return 0
5689
5690
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005691def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005692 """Prints info about try jobs associated with current CL."""
5693 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005694 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005695 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005696 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005697 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005698 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005699 '--color', action='store_true', default=setup_color.IS_TTY,
5700 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005701 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005702 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5703 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005704 group.add_option(
Stefan Zager1306bd02017-06-22 19:26:46 -07005705 '--json', help=('Path of JSON output file to write try job results to,'
5706 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005707 parser.add_option_group(group)
5708 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07005709 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005710 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07005711 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005712 if args:
5713 parser.error('Unrecognized args: %s' % ' '.join(args))
5714
5715 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07005716 cl = Changelist(
5717 issue=options.issue, codereview=options.forced_codereview,
5718 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005719 if not cl.GetIssue():
5720 parser.error('Need to upload first')
5721
tandrii221ab252016-10-06 08:12:04 -07005722 patchset = options.patchset
5723 if not patchset:
5724 patchset = cl.GetMostRecentPatchset()
5725 if not patchset:
5726 parser.error('Codereview doesn\'t know about issue %s. '
5727 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005728 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005729 cl.GetIssue())
5730
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005731 # TODO(tandrii): Checking local patchset against remote patchset is only
5732 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5733 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07005734 print('Warning: Codereview server has newer patchsets (%s) than most '
5735 'recent upload from local checkout (%s). Did a previous upload '
5736 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07005737 'By default, git cl try-results uses the latest patchset from '
5738 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07005739 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005740 try:
tandrii221ab252016-10-06 08:12:04 -07005741 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005742 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005743 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005744 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005745 if options.json:
5746 write_try_results_json(options.json, jobs)
5747 else:
5748 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005749 return 0
5750
5751
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005752@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005753def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005754 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005755 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005756 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005757 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005758
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005759 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005760 if args:
5761 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005762 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005763 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005764 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005765 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005766
5767 # Clear configured merge-base, if there is one.
5768 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005769 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005770 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005771 return 0
5772
5773
thestig@chromium.org00858c82013-12-02 23:08:03 +00005774def CMDweb(parser, args):
5775 """Opens the current CL in the web browser."""
5776 _, args = parser.parse_args(args)
5777 if args:
5778 parser.error('Unrecognized args: %s' % ' '.join(args))
5779
5780 issue_url = Changelist().GetIssueURL()
5781 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005782 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005783 return 1
5784
5785 webbrowser.open(issue_url)
5786 return 0
5787
5788
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005789def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005790 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005791 parser.add_option('-d', '--dry-run', action='store_true',
5792 help='trigger in dry run mode')
5793 parser.add_option('-c', '--clear', action='store_true',
5794 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005795 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005796 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005797 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005798 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005799 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005800 if args:
5801 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005802 if options.dry_run and options.clear:
5803 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5804
iannuccie53c9352016-08-17 14:40:40 -07005805 cl = Changelist(auth_config=auth_config, issue=options.issue,
5806 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005807 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005808 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005809 elif options.dry_run:
5810 state = _CQState.DRY_RUN
5811 else:
5812 state = _CQState.COMMIT
5813 if not cl.GetIssue():
5814 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005815 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005816 return 0
5817
5818
groby@chromium.org411034a2013-02-26 15:12:01 +00005819def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005820 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005821 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005822 auth.add_auth_options(parser)
5823 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005824 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005825 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005826 if args:
5827 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005828 cl = Changelist(auth_config=auth_config, issue=options.issue,
5829 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005830 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005831 if not cl.GetIssue():
5832 DieWithError('ERROR No issue to close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005833 cl.CloseIssue()
5834 return 0
5835
5836
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005837def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005838 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005839 parser.add_option(
5840 '--stat',
5841 action='store_true',
5842 dest='stat',
5843 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005844 auth.add_auth_options(parser)
5845 options, args = parser.parse_args(args)
5846 auth_config = auth.extract_auth_config_from_options(options)
5847 if args:
5848 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005849
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005850 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005851 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005852 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005853 if not issue:
5854 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005855
Aaron Gablea718c3e2017-08-28 17:47:28 -07005856 base = cl._GitGetBranchConfigValue('last-upload-hash')
5857 if not base:
5858 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5859 if not base:
5860 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5861 revision_info = detail['revisions'][detail['current_revision']]
5862 fetch_info = revision_info['fetch']['http']
5863 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5864 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005865
Aaron Gablea718c3e2017-08-28 17:47:28 -07005866 cmd = ['git', 'diff']
5867 if options.stat:
5868 cmd.append('--stat')
5869 cmd.append(base)
5870 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005871
5872 return 0
5873
5874
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005875def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005876 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005877 parser.add_option(
5878 '--no-color',
5879 action='store_true',
5880 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005881 parser.add_option(
5882 '--batch',
5883 action='store_true',
5884 help='Do not run interactively, just suggest some')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005885 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005886 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005887 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005888
5889 author = RunGit(['config', 'user.email']).strip() or None
5890
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005891 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005892
5893 if args:
5894 if len(args) > 1:
5895 parser.error('Unknown args')
5896 base_branch = args[0]
5897 else:
5898 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005899 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005900
5901 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005902 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5903
5904 if options.batch:
5905 db = owners.Database(change.RepositoryRoot(), file, os.path)
5906 print('\n'.join(db.reviewers_for(affected_files, author)))
5907 return 0
5908
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005909 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005910 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005911 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005912 author,
5913 cl.GetReviewers(),
5914 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005915 disable_color=options.no_color,
5916 override_files=change.OriginalOwnersFiles()).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005917
5918
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005919def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005920 """Generates a diff command."""
5921 # Generate diff for the current branch's changes.
Aaron Gablef4068aa2017-12-12 15:14:09 -08005922 diff_cmd = ['-c', 'core.quotePath=false', 'diff',
5923 '--no-ext-diff', '--no-prefix', diff_type,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005924 upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005925
5926 if args:
5927 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005928 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005929 diff_cmd.append(arg)
5930 else:
5931 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005932
5933 return diff_cmd
5934
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005935
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005936def MatchingFileType(file_name, extensions):
5937 """Returns true if the file name ends with one of the given extensions."""
5938 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005939
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005940
enne@chromium.org555cfe42014-01-29 18:21:39 +00005941@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005942def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005943 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005944 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005945 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005946 parser.add_option('--full', action='store_true',
5947 help='Reformat the full content of all touched files')
5948 parser.add_option('--dry-run', action='store_true',
5949 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005950 parser.add_option('--python', action='store_true',
5951 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005952 parser.add_option('--js', action='store_true',
5953 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005954 parser.add_option('--diff', action='store_true',
5955 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005956 parser.add_option('--presubmit', action='store_true',
5957 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005958 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005959
Daniel Chengc55eecf2016-12-30 03:11:02 -08005960 # Normalize any remaining args against the current path, so paths relative to
5961 # the current directory are still resolved as expected.
5962 args = [os.path.join(os.getcwd(), arg) for arg in args]
5963
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005964 # git diff generates paths against the root of the repository. Change
5965 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005966 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005967 if rel_base_path:
5968 os.chdir(rel_base_path)
5969
digit@chromium.org29e47272013-05-17 17:01:46 +00005970 # Grab the merge-base commit, i.e. the upstream commit of the current
5971 # branch when it was created or the last time it was rebased. This is
5972 # to cover the case where the user may have called "git fetch origin",
5973 # moving the origin branch to a newer commit, but hasn't rebased yet.
5974 upstream_commit = None
5975 cl = Changelist()
5976 upstream_branch = cl.GetUpstreamBranch()
5977 if upstream_branch:
5978 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5979 upstream_commit = upstream_commit.strip()
5980
5981 if not upstream_commit:
5982 DieWithError('Could not find base commit for this branch. '
5983 'Are you in detached state?')
5984
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005985 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5986 diff_output = RunGit(changed_files_cmd)
5987 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005988 # Filter out files deleted by this CL
5989 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005990
Christopher Lamc5ba6922017-01-24 11:19:14 +11005991 if opts.js:
5992 CLANG_EXTS.append('.js')
5993
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005994 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5995 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5996 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005997 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005998
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005999 top_dir = os.path.normpath(
6000 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
6001
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006002 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
6003 # formatted. This is used to block during the presubmit.
6004 return_value = 0
6005
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006006 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00006007 # Locate the clang-format binary in the checkout
6008 try:
6009 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07006010 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00006011 DieWithError(e)
6012
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006013 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006014 cmd = [clang_format_tool]
6015 if not opts.dry_run and not opts.diff:
6016 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006017 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006018 if opts.diff:
6019 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006020 else:
6021 env = os.environ.copy()
6022 env['PATH'] = str(os.path.dirname(clang_format_tool))
6023 try:
6024 script = clang_format.FindClangFormatScriptInChromiumTree(
6025 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07006026 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006027 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00006028
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006029 cmd = [sys.executable, script, '-p0']
6030 if not opts.dry_run and not opts.diff:
6031 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00006032
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006033 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
6034 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006035
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006036 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
6037 if opts.diff:
6038 sys.stdout.write(stdout)
6039 if opts.dry_run and len(stdout) > 0:
6040 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006041
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006042 # Similar code to above, but using yapf on .py files rather than clang-format
6043 # on C/C++ files
6044 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006045 yapf_tool = gclient_utils.FindExecutable('yapf')
6046 if yapf_tool is None:
6047 DieWithError('yapf not found in PATH')
6048
6049 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006050 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006051 cmd = [yapf_tool]
6052 if not opts.dry_run and not opts.diff:
6053 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006054 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006055 if opts.diff:
6056 sys.stdout.write(stdout)
6057 else:
6058 # TODO(sbc): yapf --lines mode still has some issues.
6059 # https://github.com/google/yapf/issues/154
6060 DieWithError('--python currently only works with --full')
6061
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006062 # Dart's formatter does not have the nice property of only operating on
6063 # modified chunks, so hard code full.
6064 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006065 try:
6066 command = [dart_format.FindDartFmtToolInChromiumTree()]
6067 if not opts.dry_run and not opts.diff:
6068 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006069 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006070
ppi@chromium.org6593d932016-03-03 15:41:15 +00006071 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006072 if opts.dry_run and stdout:
6073 return_value = 2
6074 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07006075 print('Warning: Unable to check Dart code formatting. Dart SDK not '
6076 'found in this checkout. Files in other languages are still '
6077 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006078
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006079 # Format GN build files. Always run on full build files for canonical form.
6080 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01006081 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07006082 if opts.dry_run or opts.diff:
6083 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006084 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07006085 gn_ret = subprocess2.call(cmd + [gn_diff_file],
6086 shell=sys.platform == 'win32',
6087 cwd=top_dir)
6088 if opts.dry_run and gn_ret == 2:
6089 return_value = 2 # Not formatted.
6090 elif opts.diff and gn_ret == 2:
6091 # TODO this should compute and print the actual diff.
6092 print("This change has GN build file diff for " + gn_diff_file)
6093 elif gn_ret != 0:
6094 # For non-dry run cases (and non-2 return values for dry-run), a
6095 # nonzero error code indicates a failure, probably because the file
6096 # doesn't parse.
6097 DieWithError("gn format failed on " + gn_diff_file +
6098 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006099
Ilya Shermane081cbe2017-08-15 17:51:04 -07006100 # Skip the metrics formatting from the global presubmit hook. These files have
6101 # a separate presubmit hook that issues an error if the files need formatting,
6102 # whereas the top-level presubmit script merely issues a warning. Formatting
6103 # these files is somewhat slow, so it's important not to duplicate the work.
6104 if not opts.presubmit:
6105 for xml_dir in GetDirtyMetricsDirs(diff_files):
6106 tool_dir = os.path.join(top_dir, xml_dir)
6107 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
6108 if opts.dry_run or opts.diff:
6109 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07006110 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07006111 if opts.diff:
6112 sys.stdout.write(stdout)
6113 if opts.dry_run and stdout:
6114 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05006115
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006116 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006117
Steven Holte2e664bf2017-04-21 13:10:47 -07006118def GetDirtyMetricsDirs(diff_files):
6119 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
6120 metrics_xml_dirs = [
6121 os.path.join('tools', 'metrics', 'actions'),
6122 os.path.join('tools', 'metrics', 'histograms'),
6123 os.path.join('tools', 'metrics', 'rappor'),
6124 os.path.join('tools', 'metrics', 'ukm')]
6125 for xml_dir in metrics_xml_dirs:
6126 if any(file.startswith(xml_dir) for file in xml_diff_files):
6127 yield xml_dir
6128
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006129
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006130@subcommand.usage('<codereview url or issue id>')
6131def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006132 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006133 _, args = parser.parse_args(args)
6134
6135 if len(args) != 1:
6136 parser.print_help()
6137 return 1
6138
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00006139 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00006140 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02006141 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02006142
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00006143 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006144
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006145 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00006146 output = RunGit(['config', '--local', '--get-regexp',
6147 r'branch\..*\.%s' % issueprefix],
6148 error_ok=True)
6149 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006150 if issue == target_issue:
6151 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006152
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006153 branches = []
6154 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07006155 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006156 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07006157 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006158 return 1
6159 if len(branches) == 1:
6160 RunGit(['checkout', branches[0]])
6161 else:
vapiera7fbd5a2016-06-16 09:17:49 -07006162 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006163 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07006164 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006165 which = raw_input('Choose by index: ')
6166 try:
6167 RunGit(['checkout', branches[int(which)]])
6168 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07006169 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006170 return 1
6171
6172 return 0
6173
6174
maruel@chromium.org29404b52014-09-08 22:58:00 +00006175def CMDlol(parser, args):
6176 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07006177 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00006178 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
6179 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
6180 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07006181 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00006182 return 0
6183
6184
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006185class OptionParser(optparse.OptionParser):
6186 """Creates the option parse and add --verbose support."""
6187 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006188 optparse.OptionParser.__init__(
6189 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006190 self.add_option(
6191 '-v', '--verbose', action='count', default=0,
6192 help='Use 2 times for more debugging info')
6193
6194 def parse_args(self, args=None, values=None):
6195 options, args = optparse.OptionParser.parse_args(self, args, values)
6196 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01006197 logging.basicConfig(
6198 level=levels[min(options.verbose, len(levels) - 1)],
6199 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
6200 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006201 return options, args
6202
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006203
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006204def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006205 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07006206 print('\nYour python version %s is unsupported, please upgrade.\n' %
6207 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006208 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006209
maruel@chromium.orgddd59412011-11-30 14:20:38 +00006210 # Reload settings.
6211 global settings
6212 settings = Settings()
6213
maruel@chromium.org39c0b222013-08-17 16:57:01 +00006214 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006215 dispatcher = subcommand.CommandDispatcher(__name__)
6216 try:
6217 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00006218 except auth.AuthenticationError as e:
6219 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07006220 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006221 if e.code != 500:
6222 raise
6223 DieWithError(
6224 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
6225 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00006226 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006227
6228
6229if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006230 # These affect sys.stdout so do it outside of main() to simplify mocks in
6231 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00006232 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00006233 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00006234 try:
6235 sys.exit(main(sys.argv[1:]))
6236 except KeyboardInterrupt:
6237 sys.stderr.write('interrupted\n')
6238 sys.exit(1)