blob: ba5c54f5ff2c1c63df032e7229b0e1213767d725 [file] [log] [blame]
Edward Lesmes98eda3f2019-08-12 21:09:53 +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
Andrii Shyshkalov03da1502018-10-15 03:42:34 +00008"""A git-command for integrating reviews on 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
sheyang@google.com6ebaf782015-05-12 19:17:54 +000018import httplib
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +010019import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000020import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000022import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023import optparse
24import os
25import re
Andrii Shyshkalov353637c2017-03-14 16:52:18 +010026import shutil
ukai@chromium.org78c4b982012-02-14 02:20:26 +000027import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028import sys
Aaron Gable9a03ae02017-11-03 11:31:07 -070029import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000030import textwrap
Edward Lemurfec80c42018-11-01 23:14:14 +000031import time
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
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000039from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000040from third_party import httplib2
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000041import auth
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000042import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000043import dart_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000044import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000045import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000046import gerrit_util
iannucci@chromium.org9e849272014-04-04 00:31:55 +000047import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000048import git_footers
Edward Lemur5ba1e9c2018-07-23 18:19:02 +000049import metrics
Edward Lesmes93277a72018-10-18 22:04:26 +000050import metrics_utils
piman@chromium.org336f9122014-09-04 02:16:55 +000051import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000052import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000053import presubmit_support
54import scm
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +000055import setup_color
Francois Dorayd42c6812017-05-30 15:10:20 -040056import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000057import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000058import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000059import watchlists
60
tandrii7400cf02016-06-21 08:48:07 -070061__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062
Edward Lemur0f58ae42019-04-30 17:24:12 +000063# Traces for git push will be stored in a traces directory inside the
64# depot_tools checkout.
65DEPOT_TOOLS = os.path.dirname(os.path.abspath(__file__))
66TRACES_DIR = os.path.join(DEPOT_TOOLS, 'traces')
67
68# When collecting traces, Git hashes will be reduced to 6 characters to reduce
69# the size after compression.
70GIT_HASH_RE = re.compile(r'\b([a-f0-9]{6})[a-f0-9]{34}\b', flags=re.I)
71# Used to redact the cookies from the gitcookies file.
72GITCOOKIES_REDACT_RE = re.compile(r'1/.*')
73
Edward Lemur1b52d872019-05-09 21:12:12 +000074# The maximum number of traces we will keep. Multiplied by 3 since we store
75# 3 files per trace.
76MAX_TRACES = 3 * 10
Edward Lemur5737f022019-05-17 01:24:00 +000077# Message to be displayed to the user to inform where to find the traces for a
78# git-cl upload execution.
Edward Lemur0f58ae42019-04-30 17:24:12 +000079TRACES_MESSAGE = (
Edward Lemur1b52d872019-05-09 21:12:12 +000080'\n'
Edward Lemur5737f022019-05-17 01:24:00 +000081'The traces of this git-cl execution have been recorded at:\n'
Edward Lemur1b52d872019-05-09 21:12:12 +000082' %(trace_name)s-traces.zip\n'
Edward Lemur5737f022019-05-17 01:24:00 +000083'Copies of your gitcookies file and git config have been recorded at:\n'
84' %(trace_name)s-git-info.zip\n')
Edward Lemur1b52d872019-05-09 21:12:12 +000085# Format of the message to be stored as part of the traces to give developers a
86# better context when they go through traces.
87TRACES_README_FORMAT = (
88'Date: %(now)s\n'
89'\n'
90'Change: https://%(gerrit_host)s/q/%(change_id)s\n'
91'Title: %(title)s\n'
92'\n'
93'%(description)s\n'
94'\n'
95'Execution time: %(execution_time)s\n'
96'Exit code: %(exit_code)s\n') + TRACES_MESSAGE
Edward Lemur0f58ae42019-04-30 17:24:12 +000097
tandrii9d2c7a32016-06-22 03:42:45 -070098COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080099POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000100DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +0000101REFS_THAT_ALIAS_TO_OTHER_REFS = {
102 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
103 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
104}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000105
thestig@chromium.org44202a22014-03-11 19:22:18 +0000106# Valid extensions for files we want to lint.
107DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
108DEFAULT_LINT_IGNORE_REGEX = r"$^"
109
Aiden Bennerc08566e2018-10-03 17:52:42 +0000110# File name for yapf style config files.
111YAPF_CONFIG_FILENAME = '.style.yapf'
112
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000113# Buildbucket master name prefix for Buildbot masters.
borenet6c0efe62016-10-19 08:13:29 -0700114MASTER_PREFIX = 'master.'
115
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000116# Shortcut since it quickly becomes repetitive.
maruel@chromium.org2e23ce32013-05-07 12:42:28 +0000117Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +0000118
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000119# Initialized in main()
120settings = None
121
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100122# Used by tests/git_cl_test.py to add extra logging.
123# Inside the weirdly failing test, add this:
124# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700125# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100126_IS_BEING_TESTED = False
127
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000128
Christopher Lamf732cd52017-01-24 12:40:11 +1100129def DieWithError(message, change_desc=None):
130 if change_desc:
131 SaveDescriptionBackup(change_desc)
132
vapiera7fbd5a2016-06-16 09:17:49 -0700133 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000134 sys.exit(1)
135
136
Christopher Lamf732cd52017-01-24 12:40:11 +1100137def SaveDescriptionBackup(change_desc):
138 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +0000139 print('\nsaving CL description to %s\n' % backup_path)
Christopher Lamf732cd52017-01-24 12:40:11 +1100140 backup_file = open(backup_path, 'w')
141 backup_file.write(change_desc.description)
142 backup_file.close()
143
144
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000145def GetNoGitPagerEnv():
146 env = os.environ.copy()
147 # 'cat' is a magical git string that disables pagers on all platforms.
148 env['GIT_PAGER'] = 'cat'
149 return env
150
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000151
bsep@chromium.org627d9002016-04-29 00:00:52 +0000152def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000153 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000154 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000155 except subprocess2.CalledProcessError as e:
156 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000157 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000158 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000159 'Command "%s" failed.\n%s' % (
160 ' '.join(args), error_message or e.stdout or ''))
161 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000162
163
164def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000165 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000166 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000167
168
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000169def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000170 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700171 if suppress_stderr:
172 stderr = subprocess2.VOID
173 else:
174 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000175 try:
tandrii5d48c322016-08-18 16:19:37 -0700176 (out, _), code = subprocess2.communicate(['git'] + args,
177 env=GetNoGitPagerEnv(),
178 stdout=subprocess2.PIPE,
179 stderr=stderr)
180 return code, out
181 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900182 logging.debug('Failed running %s', ['git'] + args)
tandrii5d48c322016-08-18 16:19:37 -0700183 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000184
185
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000186def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000187 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000188 return RunGitWithCode(args, suppress_stderr=True)[1]
189
190
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000191def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000192 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000193 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000194 return (version.startswith(prefix) and
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000195 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000196
197
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000198def BranchExists(branch):
199 """Return True if specified branch exists."""
200 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
201 suppress_stderr=True)
202 return not code
203
204
tandrii2a16b952016-10-19 07:09:44 -0700205def time_sleep(seconds):
206 # Use this so that it can be mocked in tests without interfering with python
207 # system machinery.
tandrii2a16b952016-10-19 07:09:44 -0700208 return time.sleep(seconds)
209
210
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000211def time_time():
212 # Use this so that it can be mocked in tests without interfering with python
213 # system machinery.
214 return time.time()
215
216
Edward Lemur1b52d872019-05-09 21:12:12 +0000217def datetime_now():
218 # Use this so that it can be mocked in tests without interfering with python
219 # system machinery.
220 return datetime.datetime.now()
221
222
maruel@chromium.org90541732011-04-01 17:54:18 +0000223def ask_for_data(prompt):
224 try:
225 return raw_input(prompt)
226 except KeyboardInterrupt:
227 # Hide the exception.
228 sys.exit(1)
229
230
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100231def confirm_or_exit(prefix='', action='confirm'):
232 """Asks user to press enter to continue or press Ctrl+C to abort."""
233 if not prefix or prefix.endswith('\n'):
234 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100235 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100236 mid = ' Press'
237 elif prefix.endswith(' '):
238 mid = 'press'
239 else:
240 mid = ' press'
241 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
242
243
244def ask_for_explicit_yes(prompt):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +0000245 """Returns whether user typed 'y' or 'yes' to confirm the given prompt."""
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100246 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
247 while True:
248 if 'yes'.startswith(result):
249 return True
250 if 'no'.startswith(result):
251 return False
252 result = ask_for_data('Please, type yes or no: ').lower()
253
254
tandrii5d48c322016-08-18 16:19:37 -0700255def _git_branch_config_key(branch, key):
256 """Helper method to return Git config key for a branch."""
257 assert branch, 'branch name is required to set git config for it'
258 return 'branch.%s.%s' % (branch, key)
259
260
261def _git_get_branch_config_value(key, default=None, value_type=str,
262 branch=False):
263 """Returns git config value of given or current branch if any.
264
265 Returns default in all other cases.
266 """
267 assert value_type in (int, str, bool)
268 if branch is False: # Distinguishing default arg value from None.
269 branch = GetCurrentBranch()
270
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000271 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700272 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000273
tandrii5d48c322016-08-18 16:19:37 -0700274 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700275 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700276 args.append('--bool')
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000277 # `git config` also has --int, but apparently git config suffers from integer
tandrii33a46ff2016-08-23 05:53:40 -0700278 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700279 args.append(_git_branch_config_key(branch, key))
280 code, out = RunGitWithCode(args)
281 if code == 0:
282 value = out.strip()
283 if value_type == int:
284 return int(value)
285 if value_type == bool:
286 return bool(value.lower() == 'true')
287 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000288 return default
289
290
tandrii5d48c322016-08-18 16:19:37 -0700291def _git_set_branch_config_value(key, value, branch=None, **kwargs):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +0000292 """Sets or unsets the git branch config value.
tandrii5d48c322016-08-18 16:19:37 -0700293
Quinten Yearsleyd242ed72019-07-25 17:17:55 +0000294 If value is None, the key will be unset, otherwise it will be set.
295 If no branch is given, the currently checked out branch is used.
tandrii5d48c322016-08-18 16:19:37 -0700296 """
297 if not branch:
298 branch = GetCurrentBranch()
299 assert branch, 'a branch name OR currently checked out branch is required'
300 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700301 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700302 if value is None:
303 args.append('--unset')
304 elif isinstance(value, bool):
305 args.append('--bool')
306 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700307 else:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000308 # `git config` also has --int, but apparently git config suffers from
309 # integer overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700310 value = str(value)
311 args.append(_git_branch_config_key(branch, key))
312 if value is not None:
313 args.append(value)
314 RunGit(args, **kwargs)
315
316
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100317def _get_committer_timestamp(commit):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700318 """Returns Unix timestamp as integer of a committer in a commit.
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100319
320 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
321 """
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000322 # Git also stores timezone offset, but it only affects visual display;
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100323 # actual point in time is defined by this timestamp only.
324 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
325
326
327def _git_amend_head(message, committer_timestamp):
328 """Amends commit with new message and desired committer_timestamp.
329
330 Sets committer timezone to UTC.
331 """
332 env = os.environ.copy()
333 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
334 return RunGit(['commit', '--amend', '-m', message], env=env)
335
336
machenbach@chromium.org45453142015-09-15 08:45:22 +0000337def _get_properties_from_options(options):
338 properties = dict(x.split('=', 1) for x in options.properties)
339 for key, val in properties.iteritems():
340 try:
341 properties[key] = json.loads(val)
342 except ValueError:
343 pass # If a value couldn't be evaluated, treat it as a string.
344 return properties
345
346
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000347def _prefix_master(master):
348 """Convert user-specified master name to full master name.
349
350 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
351 name, while the developers always use shortened master name
352 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
353 function does the conversion for buildbucket migration.
354 """
borenet6c0efe62016-10-19 08:13:29 -0700355 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000356 return master
borenet6c0efe62016-10-19 08:13:29 -0700357 return '%s%s' % (MASTER_PREFIX, master)
358
359
360def _unprefix_master(bucket):
361 """Convert bucket name to shortened master name.
362
363 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
364 name, while the developers always use shortened master name
365 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
366 function does the conversion for buildbucket migration.
367 """
368 if bucket.startswith(MASTER_PREFIX):
369 return bucket[len(MASTER_PREFIX):]
370 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000371
372
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000373def _buildbucket_retry(operation_name, http, *args, **kwargs):
374 """Retries requests to buildbucket service and returns parsed json content."""
375 try_count = 0
376 while True:
377 response, content = http.request(*args, **kwargs)
378 try:
379 content_json = json.loads(content)
380 except ValueError:
381 content_json = None
382
383 # Buildbucket could return an error even if status==200.
384 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000385 error = content_json.get('error')
386 if error.get('code') == 403:
387 raise BuildbucketResponseException(
388 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000389 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000390 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000391 raise BuildbucketResponseException(msg)
392
393 if response.status == 200:
Nodir Turakulov23d75d22018-06-04 12:37:32 -0700394 if content_json is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000395 raise BuildbucketResponseException(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000396 'Buildbucket returned invalid JSON content: %s.\n'
Nodir Turakulov9ac59792018-06-04 12:34:14 -0700397 'Please file bugs at http://crbug.com, '
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000398 'component "Infra>Platform>Buildbucket".' %
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000399 content)
400 return content_json
401 if response.status < 500 or try_count >= 2:
402 raise httplib2.HttpLib2Error(content)
403
404 # status >= 500 means transient failures.
405 logging.debug('Transient errors when %s. Will retry.', operation_name)
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000406 time_sleep(0.5 + (1.5 * try_count))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000407 try_count += 1
408 assert False, 'unreachable'
409
410
qyearsley1fdfcb62016-10-24 13:22:03 -0700411def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700412 """Returns a dict mapping bucket names to builders and tests,
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000413 for triggering tryjobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700414 """
qyearsleydd49f942016-10-28 11:57:22 -0700415 # If no bots are listed, we try to get a set of builders and tests based
416 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700417 if not options.bot:
418 change = changelist.GetChange(
419 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700420 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700421 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700422 change=change,
423 changed_files=change.LocalPaths(),
424 repository_root=settings.GetRoot(),
425 default_presubmit=None,
426 project=None,
427 verbose=options.verbose,
428 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700429 if masters is None:
430 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100431 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700432
qyearsley1fdfcb62016-10-24 13:22:03 -0700433 if options.bucket:
434 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700435 if options.master:
Andrii Shyshkalov75424372019-08-30 22:48:15 +0000436 print(
437 'WARNING: "-m <master>" option is deprecated. Use -B <bucket> instead.')
qyearsleydd49f942016-10-28 11:57:22 -0700438 return {_prefix_master(options.master): {b: [] for b in options.bot}}
Andrii Shyshkalov75424372019-08-30 22:48:15 +0000439 option_parser.error(
440 'Please specify the bucket, e.g. "-B luci.chromium.try".')
qyearsley1fdfcb62016-10-24 13:22:03 -0700441
442
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800443def _trigger_try_jobs(auth_config, changelist, buckets, options, patchset):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000444 """Sends a request to Buildbucket to trigger tryjobs for a changelist.
qyearsley1fdfcb62016-10-24 13:22:03 -0700445
446 Args:
Aaron Gablefb28d482018-04-02 13:08:06 -0700447 auth_config: AuthConfig for Buildbucket.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000448 changelist: Changelist that the tryjobs are associated with.
qyearsley1fdfcb62016-10-24 13:22:03 -0700449 buckets: A nested dict mapping bucket names to builders to tests.
450 options: Command-line options.
451 """
tandriide281ae2016-10-12 06:02:30 -0700452 assert changelist.GetIssue(), 'CL must be uploaded first'
453 codereview_url = changelist.GetCodereviewServer()
454 assert codereview_url, 'CL must be uploaded first'
455 patchset = patchset or changelist.GetMostRecentPatchset()
456 assert patchset, 'CL must be uploaded first'
457
458 codereview_host = urlparse.urlparse(codereview_url).hostname
Aaron Gablefb28d482018-04-02 13:08:06 -0700459 # Cache the buildbucket credentials under the codereview host key, so that
460 # users can use different credentials for different buckets.
tandriide281ae2016-10-12 06:02:30 -0700461 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000462 http = authenticator.authorize(httplib2.Http())
463 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700464
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000465 buildbucket_put_url = (
466 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000467 hostname=options.buildbucket_host))
Andrii Shyshkalov03da1502018-10-15 03:42:34 +0000468 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandriide281ae2016-10-12 06:02:30 -0700469 hostname=codereview_host,
470 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000471 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700472
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700473 shared_parameters_properties = changelist.GetTryJobProperties(patchset)
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800474 shared_parameters_properties['category'] = options.category
tandrii8c5a3532016-11-04 07:52:02 -0700475 if options.clobber:
476 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700477 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700478 if extra_properties:
479 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000480
481 batch_req_body = {'builds': []}
482 print_text = []
483 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700484 for bucket, builders_and_tests in sorted(buckets.iteritems()):
485 print_text.append('Bucket: %s' % bucket)
486 master = None
487 if bucket.startswith(MASTER_PREFIX):
488 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000489 for builder, tests in sorted(builders_and_tests.iteritems()):
490 print_text.append(' %s: %s' % (builder, tests))
491 parameters = {
492 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000493 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100494 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000495 'revision': options.revision,
496 }],
tandrii8c5a3532016-11-04 07:52:02 -0700497 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000498 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000499 if 'presubmit' in builder.lower():
500 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000501 if tests:
502 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700503
504 tags = [
505 'builder:%s' % builder,
506 'buildset:%s' % buildset,
507 'user_agent:git_cl_try',
508 ]
509 if master:
510 parameters['properties']['master'] = master
511 tags.append('master:%s' % master)
512
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000513 batch_req_body['builds'].append(
514 {
515 'bucket': bucket,
516 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000517 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700518 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000519 }
520 )
521
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000522 _buildbucket_retry(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000523 'triggering tryjobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000524 http,
525 buildbucket_put_url,
526 'PUT',
527 body=json.dumps(batch_req_body),
528 headers={'Content-Type': 'application/json'}
529 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000530 print_text.append('To see results here, run: git cl try-results')
531 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700532 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000533
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000534
tandrii221ab252016-10-06 08:12:04 -0700535def fetch_try_jobs(auth_config, changelist, buildbucket_host,
536 patchset=None):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000537 """Fetches tryjobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000538
qyearsley53f48a12016-09-01 10:45:13 -0700539 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000540 """
tandrii221ab252016-10-06 08:12:04 -0700541 assert buildbucket_host
542 assert changelist.GetIssue(), 'CL must be uploaded first'
543 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
544 patchset = patchset or changelist.GetMostRecentPatchset()
545 assert patchset, 'CL must be uploaded first'
546
547 codereview_url = changelist.GetCodereviewServer()
548 codereview_host = urlparse.urlparse(codereview_url).hostname
549 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000550 if authenticator.has_cached_credentials():
551 http = authenticator.authorize(httplib2.Http())
552 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700553 print('Warning: Some results might be missing because %s' %
554 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700555 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000556 http = httplib2.Http()
557
558 http.force_exception_to_status_code = True
559
Andrii Shyshkalov03da1502018-10-15 03:42:34 +0000560 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandrii221ab252016-10-06 08:12:04 -0700561 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000562 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700563 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000564 params = {'tag': 'buildset:%s' % buildset}
565
566 builds = {}
567 while True:
568 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700569 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000570 params=urllib.urlencode(params))
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000571 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000572 for build in content.get('builds', []):
573 builds[build['id']] = build
574 if 'next_cursor' in content:
575 params['start_cursor'] = content['next_cursor']
576 else:
577 break
578 return builds
579
580
qyearsleyeab3c042016-08-24 09:18:28 -0700581def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000582 """Prints nicely result of fetch_try_jobs."""
583 if not builds:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000584 print('No tryjobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000585 return
586
587 # Make a copy, because we'll be modifying builds dictionary.
588 builds = builds.copy()
589 builder_names_cache = {}
590
591 def get_builder(b):
592 try:
593 return builder_names_cache[b['id']]
594 except KeyError:
595 try:
596 parameters = json.loads(b['parameters_json'])
597 name = parameters['builder_name']
598 except (ValueError, KeyError) as error:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700599 print('WARNING: Failed to get builder name for build %s: %s' % (
vapiera7fbd5a2016-06-16 09:17:49 -0700600 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000601 name = None
602 builder_names_cache[b['id']] = name
603 return name
604
605 def get_bucket(b):
606 bucket = b['bucket']
607 if bucket.startswith('master.'):
608 return bucket[len('master.'):]
609 return bucket
610
611 if options.print_master:
612 name_fmt = '%%-%ds %%-%ds' % (
613 max(len(str(get_bucket(b))) for b in builds.itervalues()),
614 max(len(str(get_builder(b))) for b in builds.itervalues()))
615 def get_name(b):
616 return name_fmt % (get_bucket(b), get_builder(b))
617 else:
618 name_fmt = '%%-%ds' % (
619 max(len(str(get_builder(b))) for b in builds.itervalues()))
620 def get_name(b):
621 return name_fmt % get_builder(b)
622
623 def sort_key(b):
624 return b['status'], b.get('result'), get_name(b), b.get('url')
625
626 def pop(title, f, color=None, **kwargs):
627 """Pop matching builds from `builds` dict and print them."""
628
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000629 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000630 colorize = str
631 else:
632 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
633
634 result = []
635 for b in builds.values():
636 if all(b.get(k) == v for k, v in kwargs.iteritems()):
637 builds.pop(b['id'])
638 result.append(b)
639 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700640 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000641 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700642 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000643
644 total = len(builds)
645 pop(status='COMPLETED', result='SUCCESS',
646 title='Successes:', color=Fore.GREEN,
647 f=lambda b: (get_name(b), b.get('url')))
648 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
649 title='Infra Failures:', color=Fore.MAGENTA,
650 f=lambda b: (get_name(b), b.get('url')))
651 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
652 title='Failures:', color=Fore.RED,
653 f=lambda b: (get_name(b), b.get('url')))
654 pop(status='COMPLETED', result='CANCELED',
655 title='Canceled:', color=Fore.MAGENTA,
656 f=lambda b: (get_name(b),))
657 pop(status='COMPLETED', result='FAILURE',
658 failure_reason='INVALID_BUILD_DEFINITION',
659 title='Wrong master/builder name:', color=Fore.MAGENTA,
660 f=lambda b: (get_name(b),))
661 pop(status='COMPLETED', result='FAILURE',
662 title='Other failures:',
663 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
664 pop(status='COMPLETED',
665 title='Other finished:',
666 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
667 pop(status='STARTED',
668 title='Started:', color=Fore.YELLOW,
669 f=lambda b: (get_name(b), b.get('url')))
670 pop(status='SCHEDULED',
671 title='Scheduled:',
672 f=lambda b: (get_name(b), 'id=%s' % b['id']))
673 # The last section is just in case buildbucket API changes OR there is a bug.
674 pop(title='Other:',
675 f=lambda b: (get_name(b), 'id=%s' % b['id']))
676 assert len(builds) == 0
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000677 print('Total: %d tryjobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000678
679
Aiden Bennerc08566e2018-10-03 17:52:42 +0000680def _ComputeDiffLineRanges(files, upstream_commit):
681 """Gets the changed line ranges for each file since upstream_commit.
682
683 Parses a git diff on provided files and returns a dict that maps a file name
684 to an ordered list of range tuples in the form (start_line, count).
685 Ranges are in the same format as a git diff.
686 """
687 # If files is empty then diff_output will be a full diff.
688 if len(files) == 0:
689 return {}
690
Aiden Benner6c18a1a2018-11-23 20:18:23 +0000691 # Take the git diff and find the line ranges where there are changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000692 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
693 diff_output = RunGit(diff_cmd)
694
695 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
696 # 2 capture groups
697 # 0 == fname of diff file
698 # 1 == 'diff_start,diff_count' or 'diff_start'
699 # will match each of
700 # diff --git a/foo.foo b/foo.py
701 # @@ -12,2 +14,3 @@
702 # @@ -12,2 +17 @@
703 # running re.findall on the above string with pattern will give
704 # [('foo.py', ''), ('', '14,3'), ('', '17')]
705
706 curr_file = None
707 line_diffs = {}
708 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
709 if match[0] != '':
710 # Will match the second filename in diff --git a/a.py b/b.py.
711 curr_file = match[0]
712 line_diffs[curr_file] = []
713 else:
714 # Matches +14,3
715 if ',' in match[1]:
716 diff_start, diff_count = match[1].split(',')
717 else:
718 # Single line changes are of the form +12 instead of +12,1.
719 diff_start = match[1]
720 diff_count = 1
721
722 diff_start = int(diff_start)
723 diff_count = int(diff_count)
724
725 # If diff_count == 0 this is a removal we can ignore.
726 line_diffs[curr_file].append((diff_start, diff_count))
727
728 return line_diffs
729
730
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000731def _FindYapfConfigFile(fpath, yapf_config_cache, top_dir=None):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000732 """Checks if a yapf file is in any parent directory of fpath until top_dir.
733
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000734 Recursively checks parent directories to find yapf file and if no yapf file
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000735 is found returns None. Uses yapf_config_cache as a cache for previously found
736 configs.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000737 """
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000738 fpath = os.path.abspath(fpath)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000739 # Return result if we've already computed it.
740 if fpath in yapf_config_cache:
741 return yapf_config_cache[fpath]
742
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000743 parent_dir = os.path.dirname(fpath)
744 if os.path.isfile(fpath):
745 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000746 else:
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000747 # Otherwise fpath is a directory
748 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
749 if os.path.isfile(yapf_file):
750 ret = yapf_file
751 elif fpath == top_dir or parent_dir == fpath:
752 # If we're at the top level directory, or if we're at root
753 # there is no provided style.
754 ret = None
755 else:
756 # Otherwise recurse on the current directory.
757 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000758 yapf_config_cache[fpath] = ret
759 return ret
760
761
qyearsley53f48a12016-09-01 10:45:13 -0700762def write_try_results_json(output_file, builds):
763 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
764
765 The input |builds| dict is assumed to be generated by Buildbucket.
766 Buildbucket documentation: http://goo.gl/G0s101
767 """
768
769 def convert_build_dict(build):
Quinten Yearsleya563d722017-12-11 16:36:54 -0800770 """Extracts some of the information from one build dict."""
771 parameters = json.loads(build.get('parameters_json', '{}')) or {}
qyearsley53f48a12016-09-01 10:45:13 -0700772 return {
773 'buildbucket_id': build.get('id'),
qyearsley53f48a12016-09-01 10:45:13 -0700774 'bucket': build.get('bucket'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800775 'builder_name': parameters.get('builder_name'),
776 'created_ts': build.get('created_ts'),
777 'experimental': build.get('experimental'),
qyearsley53f48a12016-09-01 10:45:13 -0700778 'failure_reason': build.get('failure_reason'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800779 'result': build.get('result'),
780 'status': build.get('status'),
781 'tags': build.get('tags'),
qyearsley53f48a12016-09-01 10:45:13 -0700782 'url': build.get('url'),
783 }
784
785 converted = []
786 for _, build in sorted(builds.items()):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000787 converted.append(convert_build_dict(build))
qyearsley53f48a12016-09-01 10:45:13 -0700788 write_json(output_file, converted)
789
790
Aaron Gable13101a62018-02-09 13:20:41 -0800791def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000792 """Prints statistics about the change to the user."""
793 # --no-ext-diff is broken in some versions of Git, so try to work around
794 # this by overriding the environment (but there is still a problem if the
795 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000796 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000797 if 'GIT_EXTERNAL_DIFF' in env:
798 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000799
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000800 try:
801 stdout = sys.stdout.fileno()
802 except AttributeError:
803 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000804 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800805 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000806 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000807
808
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000809class BuildbucketResponseException(Exception):
810 pass
811
812
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000813class Settings(object):
814 def __init__(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000815 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000816 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000817 self.tree_status_url = None
818 self.viewvc_url = None
819 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000820 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000821 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000822 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000823 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000824
825 def LazyUpdateIfNeeded(self):
826 """Updates the settings from a codereview.settings file, if available."""
827 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000828 # The only value that actually changes the behavior is
829 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000830 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000831 error_ok=True
832 ).strip().lower()
833
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000834 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000835 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000836 LoadCodereviewSettingsFromFile(cr_settings_file)
837 self.updated = True
838
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000839 @staticmethod
840 def GetRelativeRoot():
841 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000842
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000843 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000844 if self.root is None:
845 self.root = os.path.abspath(self.GetRelativeRoot())
846 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000847
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000848 def GetTreeStatusUrl(self, error_ok=False):
849 if not self.tree_status_url:
850 error_message = ('You must configure your tree status URL by running '
851 '"git cl config".')
Edward Lemur61ea3072018-12-01 00:34:36 +0000852 self.tree_status_url = self._GetConfig(
853 'rietveld.tree-status-url', error_ok=error_ok,
854 error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000855 return self.tree_status_url
856
857 def GetViewVCUrl(self):
858 if not self.viewvc_url:
Edward Lemur61ea3072018-12-01 00:34:36 +0000859 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000860 return self.viewvc_url
861
rmistry@google.com90752582014-01-14 21:04:50 +0000862 def GetBugPrefix(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000863 return self._GetConfig('rietveld.bug-prefix', error_ok=True)
rmistry@google.com78948ed2015-07-08 23:09:57 +0000864
rmistry@google.com5626a922015-02-26 14:03:30 +0000865 def GetRunPostUploadHook(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000866 run_post_upload_hook = self._GetConfig(
867 'rietveld.run-post-upload-hook', error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +0000868 return run_post_upload_hook == "True"
869
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000870 def GetDefaultCCList(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000871 return self._GetConfig('rietveld.cc', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000872
ukai@chromium.orge8077812012-02-03 03:41:46 +0000873 def GetIsGerrit(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000874 """Returns True if this repo is associated with Gerrit."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000875 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700876 self.is_gerrit = (
877 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000878 return self.is_gerrit
879
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000880 def GetSquashGerritUploads(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000881 """Returns True if uploads to Gerrit should be squashed by default."""
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000882 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700883 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
884 if self.squash_gerrit_uploads is None:
885 # Default is squash now (http://crbug.com/611892#c23).
886 self.squash_gerrit_uploads = not (
887 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
888 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000889 return self.squash_gerrit_uploads
890
tandriia60502f2016-06-20 02:01:53 -0700891 def GetSquashGerritUploadsOverride(self):
892 """Return True or False if codereview.settings should be overridden.
893
894 Returns None if no override has been defined.
895 """
896 # See also http://crbug.com/611892#c23
897 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
898 error_ok=True).strip()
899 if result == 'true':
900 return True
901 if result == 'false':
902 return False
903 return None
904
tandrii@chromium.org28253532016-04-14 13:46:56 +0000905 def GetGerritSkipEnsureAuthenticated(self):
906 """Return True if EnsureAuthenticated should not be done for Gerrit
907 uploads."""
908 if self.gerrit_skip_ensure_authenticated is None:
909 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000910 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000911 error_ok=True).strip() == 'true')
912 return self.gerrit_skip_ensure_authenticated
913
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000914 def GetGitEditor(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000915 """Returns the editor specified in the git config, or None if none is."""
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000916 if self.git_editor is None:
Raul Tambre5a525872019-02-12 19:08:08 +0000917 # Git requires single quotes for paths with spaces. We need to replace
918 # them with double quotes for Windows to treat such paths as a single
919 # path.
920 self.git_editor = self._GetConfig(
921 'core.editor', error_ok=True).replace('\'', '"')
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000922 return self.git_editor or None
923
thestig@chromium.org44202a22014-03-11 19:22:18 +0000924 def GetLintRegex(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000925 return (self._GetConfig('rietveld.cpplint-regex', error_ok=True) or
thestig@chromium.org44202a22014-03-11 19:22:18 +0000926 DEFAULT_LINT_REGEX)
927
928 def GetLintIgnoreRegex(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000929 return (self._GetConfig('rietveld.cpplint-ignore-regex', error_ok=True) or
thestig@chromium.org44202a22014-03-11 19:22:18 +0000930 DEFAULT_LINT_IGNORE_REGEX)
931
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000932 def _GetConfig(self, param, **kwargs):
933 self.LazyUpdateIfNeeded()
934 return RunGit(['config', param], **kwargs).strip()
935
936
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100937@contextlib.contextmanager
938def _get_gerrit_project_config_file(remote_url):
939 """Context manager to fetch and store Gerrit's project.config from
940 refs/meta/config branch and store it in temp file.
941
942 Provides a temporary filename or None if there was error.
943 """
944 error, _ = RunGitWithCode([
945 'fetch', remote_url,
946 '+refs/meta/config:refs/git_cl/meta/config'])
947 if error:
948 # Ref doesn't exist or isn't accessible to current user.
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700949 print('WARNING: Failed to fetch project config for %s: %s' %
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100950 (remote_url, error))
951 yield None
952 return
953
954 error, project_config_data = RunGitWithCode(
955 ['show', 'refs/git_cl/meta/config:project.config'])
956 if error:
957 print('WARNING: project.config file not found')
958 yield None
959 return
960
961 with gclient_utils.temporary_directory() as tempdir:
962 project_config_file = os.path.join(tempdir, 'project.config')
963 gclient_utils.FileWrite(project_config_file, project_config_data)
964 yield project_config_file
965
966
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000967def ShortBranchName(branch):
968 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000969 return branch.replace('refs/heads/', '', 1)
970
971
972def GetCurrentBranchRef():
973 """Returns branch ref (e.g., refs/heads/master) or None."""
974 return RunGit(['symbolic-ref', 'HEAD'],
975 stderr=subprocess2.VOID, error_ok=True).strip() or None
976
977
978def GetCurrentBranch():
979 """Returns current branch or None.
980
981 For refs/heads/* branches, returns just last part. For others, full ref.
982 """
983 branchref = GetCurrentBranchRef()
984 if branchref:
985 return ShortBranchName(branchref)
986 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000987
988
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000989class _CQState(object):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +0000990 """Enum for states of CL with respect to CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000991 NONE = 'none'
992 DRY_RUN = 'dry_run'
993 COMMIT = 'commit'
994
995 ALL_STATES = [NONE, DRY_RUN, COMMIT]
996
997
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000998class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +0200999 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001000 self.issue = issue
1001 self.patchset = patchset
1002 self.hostname = hostname
Andrii Shyshkalovf5569d22018-10-15 03:35:23 +00001003 assert codereview in (None, 'gerrit', 'rietveld')
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001004 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001005
1006 @property
1007 def valid(self):
1008 return self.issue is not None
1009
1010
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001011def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001012 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1013 fail_result = _ParsedIssueNumberArgument()
1014
1015 if arg.isdigit():
Aaron Gableaee6c852017-06-26 12:49:01 -07001016 return _ParsedIssueNumberArgument(issue=int(arg), codereview=codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001017 if not arg.startswith('http'):
1018 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -07001019
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001020 url = gclient_utils.UpgradeToHttps(arg)
1021 try:
1022 parsed_url = urlparse.urlparse(url)
1023 except ValueError:
1024 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001025
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001026 if codereview is not None:
1027 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
1028 return parsed or fail_result
1029
Andrii Shyshkalov0a264d82018-11-21 00:36:16 +00001030 return _GerritChangelistImpl.ParseIssueURL(parsed_url) or fail_result
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001031
1032
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00001033def _create_description_from_log(args):
1034 """Pulls out the commit log to use as a base for the CL description."""
1035 log_args = []
1036 if len(args) == 1 and not args[0].endswith('.'):
1037 log_args = [args[0] + '..']
1038 elif len(args) == 1 and args[0].endswith('...'):
1039 log_args = [args[0][:-1]]
1040 elif len(args) == 2:
1041 log_args = [args[0] + '..' + args[1]]
1042 else:
1043 log_args = args[:] # Hope for the best!
1044 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
1045
1046
Aaron Gablea45ee112016-11-22 15:14:38 -08001047class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001048 def __init__(self, issue, url):
1049 self.issue = issue
1050 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001051 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001052
1053 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001054 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001055 self.issue, self.url)
1056
1057
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001058_CommentSummary = collections.namedtuple(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001059 '_CommentSummary', ['date', 'message', 'sender', 'autogenerated',
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001060 # TODO(tandrii): these two aren't known in Gerrit.
1061 'approval', 'disapproval'])
1062
1063
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001064class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001065 """Changelist works with one changelist in local branch.
1066
1067 Supports two codereview backends: Rietveld or Gerrit, selected at object
1068 creation.
1069
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001070 Notes:
1071 * Not safe for concurrent multi-{thread,process} use.
1072 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001073 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001074 """
1075
1076 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1077 """Create a new ChangeList instance.
1078
1079 If issue is given, the codereview must be given too.
1080
1081 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1082 Otherwise, it's decided based on current configuration of the local branch,
1083 with default being 'rietveld' for backwards compatibility.
1084 See _load_codereview_impl for more details.
1085
1086 **kwargs will be passed directly to codereview implementation.
1087 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001088 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001089 global settings
1090 if not settings:
1091 # Happens when git_cl.py is used as a utility library.
1092 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001093
1094 if issue:
1095 assert codereview, 'codereview must be known, if issue is known'
1096
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001097 self.branchref = branchref
1098 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001099 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001100 self.branch = ShortBranchName(self.branchref)
1101 else:
1102 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001103 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001104 self.lookedup_issue = False
1105 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001106 self.has_description = False
1107 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001108 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001109 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001110 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001111 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001112 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001113 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001114
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001115 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001116 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001117 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001118 assert self._codereview_impl
1119 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001120
1121 def _load_codereview_impl(self, codereview=None, **kwargs):
1122 if codereview:
Joe Masond87b0962018-12-03 21:04:46 +00001123 assert codereview in _CODEREVIEW_IMPLEMENTATIONS, (
1124 'codereview {} not in {}'.format(codereview,
1125 _CODEREVIEW_IMPLEMENTATIONS))
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001126 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1127 self._codereview = codereview
1128 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001129 return
1130
1131 # Automatic selection based on issue number set for a current branch.
1132 # Rietveld takes precedence over Gerrit.
1133 assert not self.issue
1134 # Whether we find issue or not, we are doing the lookup.
1135 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001136 if self.GetBranch():
1137 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1138 issue = _git_get_branch_config_value(
1139 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1140 if issue:
1141 self._codereview = codereview
1142 self._codereview_impl = cls(self, **kwargs)
1143 self.issue = int(issue)
1144 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001145
Bryce Thomascfc97122018-12-13 20:21:47 +00001146 # No issue is set for this branch, so default to gerrit.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001147 return self._load_codereview_impl(
Bryce Thomascfc97122018-12-13 20:21:47 +00001148 codereview='gerrit',
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001149 **kwargs)
1150
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001151 def IsGerrit(self):
1152 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001153
1154 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001155 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001156
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001157 The return value is a string suitable for passing to git cl with the --cc
1158 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001159 """
1160 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001161 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001162 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001163 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1164 return self.cc
1165
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001166 def GetCCListWithoutDefault(self):
1167 """Return the users cc'd on this CL excluding default ones."""
1168 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001169 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001170 return self.cc
1171
Daniel Cheng7227d212017-11-17 08:12:37 -08001172 def ExtendCC(self, more_cc):
1173 """Extends the list of users to cc on this CL based on the changed files."""
1174 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001175
1176 def GetBranch(self):
1177 """Returns the short branch name, e.g. 'master'."""
1178 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001179 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001180 if not branchref:
1181 return None
1182 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001183 self.branch = ShortBranchName(self.branchref)
1184 return self.branch
1185
1186 def GetBranchRef(self):
1187 """Returns the full branch name, e.g. 'refs/heads/master'."""
1188 self.GetBranch() # Poke the lazy loader.
1189 return self.branchref
1190
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001191 def ClearBranch(self):
1192 """Clears cached branch data of this object."""
1193 self.branch = self.branchref = None
1194
tandrii5d48c322016-08-18 16:19:37 -07001195 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1196 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1197 kwargs['branch'] = self.GetBranch()
1198 return _git_get_branch_config_value(key, default, **kwargs)
1199
1200 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1201 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1202 assert self.GetBranch(), (
1203 'this CL must have an associated branch to %sset %s%s' %
1204 ('un' if value is None else '',
1205 key,
1206 '' if value is None else ' to %r' % value))
1207 kwargs['branch'] = self.GetBranch()
1208 return _git_set_branch_config_value(key, value, **kwargs)
1209
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001210 @staticmethod
1211 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001212 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001213 e.g. 'origin', 'refs/heads/master'
1214 """
1215 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001216 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1217
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001218 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001219 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001220 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001221 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1222 error_ok=True).strip()
1223 if upstream_branch:
1224 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001225 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001226 # Else, try to guess the origin remote.
1227 remote_branches = RunGit(['branch', '-r']).split()
1228 if 'origin/master' in remote_branches:
1229 # Fall back on origin/master if it exits.
1230 remote = 'origin'
1231 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001232 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001233 DieWithError(
1234 'Unable to determine default branch to diff against.\n'
1235 'Either pass complete "git diff"-style arguments, like\n'
1236 ' git cl upload origin/master\n'
1237 'or verify this branch is set up to track another \n'
1238 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001239
1240 return remote, upstream_branch
1241
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001242 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001243 upstream_branch = self.GetUpstreamBranch()
1244 if not BranchExists(upstream_branch):
1245 DieWithError('The upstream for the current branch (%s) does not exist '
1246 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001247 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001248 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001249
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001250 def GetUpstreamBranch(self):
1251 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001252 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Raul Tambrefe1dbe12019-05-02 04:43:57 +00001253 if remote != '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001254 upstream_branch = upstream_branch.replace('refs/heads/',
1255 'refs/remotes/%s/' % remote)
1256 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1257 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001258 self.upstream_branch = upstream_branch
1259 return self.upstream_branch
1260
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001261 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001262 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001263 remote, branch = None, self.GetBranch()
1264 seen_branches = set()
1265 while branch not in seen_branches:
1266 seen_branches.add(branch)
1267 remote, branch = self.FetchUpstreamTuple(branch)
1268 branch = ShortBranchName(branch)
1269 if remote != '.' or branch.startswith('refs/remotes'):
1270 break
1271 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001272 remotes = RunGit(['remote'], error_ok=True).split()
1273 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001274 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001275 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001276 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001277 logging.warn('Could not determine which remote this change is '
1278 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001279 else:
1280 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001281 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001282 branch = 'HEAD'
1283 if branch.startswith('refs/remotes'):
1284 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001285 elif branch.startswith('refs/branch-heads/'):
1286 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001287 else:
1288 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001289 return self._remote
1290
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001291 def GitSanityChecks(self, upstream_git_obj):
1292 """Checks git repo status and ensures diff is from local commits."""
1293
sbc@chromium.org79706062015-01-14 21:18:12 +00001294 if upstream_git_obj is None:
1295 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001296 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001297 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001298 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001299 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001300 return False
1301
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001302 # Verify the commit we're diffing against is in our current branch.
1303 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1304 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1305 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001306 print('ERROR: %s is not in the current branch. You may need to rebase '
1307 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001308 return False
1309
1310 # List the commits inside the diff, and verify they are all local.
1311 commits_in_diff = RunGit(
1312 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1313 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1314 remote_branch = remote_branch.strip()
1315 if code != 0:
1316 _, remote_branch = self.GetRemoteBranch()
1317
1318 commits_in_remote = RunGit(
1319 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1320
1321 common_commits = set(commits_in_diff) & set(commits_in_remote)
1322 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001323 print('ERROR: Your diff contains %d commits already in %s.\n'
1324 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1325 'the diff. If you are using a custom git flow, you can override'
1326 ' the reference used for this check with "git config '
1327 'gitcl.remotebranch <git-ref>".' % (
1328 len(common_commits), remote_branch, upstream_git_obj),
1329 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001330 return False
1331 return True
1332
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001333 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001334 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001335
1336 Returns None if it is not set.
1337 """
tandrii5d48c322016-08-18 16:19:37 -07001338 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001339
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001340 def GetRemoteUrl(self):
1341 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1342
1343 Returns None if there is no remote.
1344 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001345 is_cached, value = self._cached_remote_url
1346 if is_cached:
1347 return value
1348
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001349 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001350 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1351
Edward Lemur298f2cf2019-02-22 21:40:39 +00001352 # Check if the remote url can be parsed as an URL.
1353 host = urlparse.urlparse(url).netloc
1354 if host:
1355 self._cached_remote_url = (True, url)
1356 return url
1357
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001358 # If it cannot be parsed as an url, assume it is a local directory,
1359 # probably a git cache.
Edward Lemur298f2cf2019-02-22 21:40:39 +00001360 logging.warning('"%s" doesn\'t appear to point to a git host. '
1361 'Interpreting it as a local directory.', url)
1362 if not os.path.isdir(url):
1363 logging.error(
1364 'Remote "%s" for branch "%s" points to "%s", but it doesn\'t exist.',
1365 remote, url, self.GetBranch())
1366 return None
1367
1368 cache_path = url
1369 url = RunGit(['config', 'remote.%s.url' % remote],
1370 error_ok=True,
1371 cwd=url).strip()
1372
1373 host = urlparse.urlparse(url).netloc
1374 if not host:
1375 logging.error(
1376 'Remote "%(remote)s" for branch "%(branch)s" points to '
1377 '"%(cache_path)s", but it is misconfigured.\n'
1378 '"%(cache_path)s" must be a git repo and must have a remote named '
1379 '"%(remote)s" pointing to the git host.', {
1380 'remote': remote,
1381 'cache_path': cache_path,
1382 'branch': self.GetBranch()})
1383 return None
1384
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001385 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001386 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001387
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001388 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001389 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001390 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001391 self.issue = self._GitGetBranchConfigValue(
1392 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001393 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001394 return self.issue
1395
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001396 def GetIssueURL(self):
1397 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001398 issue = self.GetIssue()
1399 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001400 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001401 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001402
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001403 def GetDescription(self, pretty=False, force=False):
1404 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001405 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001406 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001407 self.has_description = True
1408 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001409 # Set width to 72 columns + 2 space indent.
1410 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001411 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001412 lines = self.description.splitlines()
1413 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001414 return self.description
1415
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001416 def GetDescriptionFooters(self):
1417 """Returns (non_footer_lines, footers) for the commit message.
1418
1419 Returns:
1420 non_footer_lines (list(str)) - Simple list of description lines without
1421 any footer. The lines do not contain newlines, nor does the list contain
1422 the empty line between the message and the footers.
1423 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1424 [("Change-Id", "Ideadbeef...."), ...]
1425 """
1426 raw_description = self.GetDescription()
1427 msg_lines, _, footers = git_footers.split_footers(raw_description)
1428 if footers:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001429 msg_lines = msg_lines[:len(msg_lines) - 1]
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001430 return msg_lines, footers
1431
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001432 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001433 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001434 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001435 self.patchset = self._GitGetBranchConfigValue(
1436 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001437 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001438 return self.patchset
1439
1440 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001441 """Set this branch's patchset. If patchset=0, clears the patchset."""
1442 assert self.GetBranch()
1443 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001444 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001445 else:
1446 self.patchset = int(patchset)
1447 self._GitSetBranchConfigValue(
1448 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001449
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001450 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001451 """Set this branch's issue. If issue isn't given, clears the issue."""
1452 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001453 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001454 issue = int(issue)
1455 self._GitSetBranchConfigValue(
1456 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001457 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001458 codereview_server = self._codereview_impl.GetCodereviewServer()
1459 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001460 self._GitSetBranchConfigValue(
1461 self._codereview_impl.CodereviewServerConfigKey(),
1462 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001463 else:
tandrii5d48c322016-08-18 16:19:37 -07001464 # Reset all of these just to be clean.
1465 reset_suffixes = [
1466 'last-upload-hash',
1467 self._codereview_impl.IssueConfigKey(),
1468 self._codereview_impl.PatchsetConfigKey(),
1469 self._codereview_impl.CodereviewServerConfigKey(),
1470 ] + self._PostUnsetIssueProperties()
1471 for prop in reset_suffixes:
1472 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001473 msg = RunGit(['log', '-1', '--format=%B']).strip()
1474 if msg and git_footers.get_footer_change_id(msg):
1475 print('WARNING: The change patched into this branch has a Change-Id. '
1476 'Removing it.')
1477 RunGit(['commit', '--amend', '-m',
1478 git_footers.remove_footer(msg, 'Change-Id')])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001479 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001480 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001481
dnjba1b0f32016-09-02 12:37:42 -07001482 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001483 if not self.GitSanityChecks(upstream_branch):
1484 DieWithError('\nGit sanity check failure')
1485
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001486 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001487 if not root:
1488 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001489 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001490
1491 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001492 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001493 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001494 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001495 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001496 except subprocess2.CalledProcessError:
1497 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001498 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001499 'This branch probably doesn\'t exist anymore. To reset the\n'
1500 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001501 ' git branch --set-upstream-to origin/master %s\n'
1502 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001503 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001504
maruel@chromium.org52424302012-08-29 15:14:30 +00001505 issue = self.GetIssue()
1506 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001507 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001508 description = self.GetDescription()
1509 else:
1510 # If the change was never uploaded, use the log messages of all commits
1511 # up to the branch point, as git cl upload will prefill the description
1512 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001513 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1514 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001515
1516 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001517 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001518 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001519 name,
1520 description,
1521 absroot,
1522 files,
1523 issue,
1524 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001525 author,
1526 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001527
dsansomee2d6fd92016-09-08 00:10:47 -07001528 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001529 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001530 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001531 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001532
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001533 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1534 """Sets the description for this CL remotely.
1535
1536 You can get description_lines and footers with GetDescriptionFooters.
1537
1538 Args:
1539 description_lines (list(str)) - List of CL description lines without
1540 newline characters.
1541 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1542 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1543 `List-Of-Tokens`). It will be case-normalized so that each token is
1544 title-cased.
1545 """
1546 new_description = '\n'.join(description_lines)
1547 if footers:
1548 new_description += '\n'
1549 for k, v in footers:
1550 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1551 if not git_footers.FOOTER_PATTERN.match(foot):
1552 raise ValueError('Invalid footer %r' % foot)
1553 new_description += foot + '\n'
1554 self.UpdateDescription(new_description, force)
1555
Edward Lesmes8e282792018-04-03 18:50:29 -04001556 def RunHook(self, committing, may_prompt, verbose, change, parallel):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001557 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1558 try:
Edward Lemur2c48f242019-06-04 16:14:09 +00001559 start = time_time()
1560 result = presubmit_support.DoPresubmitChecks(change, committing,
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001561 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1562 default_presubmit=None, may_prompt=may_prompt,
Edward Lesmes8e282792018-04-03 18:50:29 -04001563 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit(),
1564 parallel=parallel)
Edward Lemur2c48f242019-06-04 16:14:09 +00001565 metrics.collector.add_repeated('sub_commands', {
1566 'command': 'presubmit',
1567 'execution_time': time_time() - start,
1568 'exit_code': 0 if result.should_continue() else 1,
1569 })
1570 return result
vapierfd77ac72016-06-16 08:33:57 -07001571 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001572 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001573
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001574 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1575 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001576 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1577 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001578 else:
1579 # Assume url.
1580 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1581 urlparse.urlparse(issue_arg))
1582 if not parsed_issue_arg or not parsed_issue_arg.valid:
1583 DieWithError('Failed to parse issue argument "%s". '
1584 'Must be an issue number or a valid URL.' % issue_arg)
1585 return self._codereview_impl.CMDPatchWithParsedIssue(
Aaron Gable62619a32017-06-16 08:22:09 -07001586 parsed_issue_arg, reject, nocommit, directory, False)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001587
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001588 def CMDUpload(self, options, git_diff_args, orig_args):
1589 """Uploads a change to codereview."""
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00001590 assert self.IsGerrit()
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001591 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001592 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001593 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001594 else:
1595 if self.GetBranch() is None:
1596 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1597
1598 # Default to diffing against common ancestor of upstream branch
1599 base_branch = self.GetCommonAncestorWithUpstream()
1600 git_diff_args = [base_branch, 'HEAD']
1601
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001602 # Fast best-effort checks to abort before running potentially expensive
1603 # hooks if uploading is likely to fail anyway. Passing these checks does
1604 # not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001605 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001606 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001607
1608 # Apply watchlists on upload.
1609 change = self.GetChange(base_branch, None)
1610 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1611 files = [f.LocalPath() for f in change.AffectedFiles()]
1612 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001613 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001614
1615 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001616 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001617 # Set the reviewer list now so that presubmit checks can access it.
1618 change_description = ChangeDescription(change.FullDescriptionText())
1619 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001620 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001621 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001622 change)
1623 change.SetDescriptionText(change_description.description)
1624 hook_results = self.RunHook(committing=False,
Edward Lesmes8e282792018-04-03 18:50:29 -04001625 may_prompt=not options.force,
1626 verbose=options.verbose,
1627 change=change, parallel=options.parallel)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001628 if not hook_results.should_continue():
1629 return 1
1630 if not options.reviewers and hook_results.reviewers:
1631 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001632 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001633
Aaron Gable13101a62018-02-09 13:20:41 -08001634 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001635 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001636 if not ret:
tandrii5d48c322016-08-18 16:19:37 -07001637 _git_set_branch_config_value('last-upload-hash',
1638 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001639 # Run post upload hooks, if specified.
1640 if settings.GetRunPostUploadHook():
1641 presubmit_support.DoPostUploadExecuter(
1642 change,
1643 self,
1644 settings.GetRoot(),
1645 options.verbose,
1646 sys.stdout)
1647
1648 # Upload all dependencies if specified.
1649 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001650 print()
1651 print('--dependencies has been specified.')
1652 print('All dependent local branches will be re-uploaded.')
1653 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001654 # Remove the dependencies flag from args so that we do not end up in a
1655 # loop.
1656 orig_args.remove('--dependencies')
1657 ret = upload_branch_deps(self, orig_args)
1658 return ret
1659
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001660 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001661 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001662
1663 Issue must have been already uploaded and known.
1664 """
1665 assert new_state in _CQState.ALL_STATES
1666 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001667 try:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001668 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 13:22:03 -07001669 return 0
1670 except KeyboardInterrupt:
1671 raise
1672 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001673 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001674 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001675 ' * Your project has no CQ,\n'
1676 ' * You don\'t have permission to change the CQ state,\n'
1677 ' * There\'s a bug in this code (see stack trace below).\n'
1678 'Consider specifying which bots to trigger manually or asking your '
1679 'project owners for permissions or contacting Chrome Infra at:\n'
1680 'https://www.chromium.org/infra\n\n' %
1681 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001682 # Still raise exception so that stack trace is printed.
1683 raise
1684
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001685 # Forward methods to codereview specific implementation.
1686
Aaron Gable636b13f2017-07-14 10:42:48 -07001687 def AddComment(self, message, publish=None):
1688 return self._codereview_impl.AddComment(message, publish=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001689
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001690 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001691 """Returns list of _CommentSummary for each comment.
1692
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001693 args:
1694 readable: determines whether the output is designed for a human or a machine
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001695 """
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001696 return self._codereview_impl.GetCommentsSummary(readable)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001697
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001698 def CloseIssue(self):
1699 return self._codereview_impl.CloseIssue()
1700
1701 def GetStatus(self):
1702 return self._codereview_impl.GetStatus()
1703
1704 def GetCodereviewServer(self):
1705 return self._codereview_impl.GetCodereviewServer()
1706
tandriide281ae2016-10-12 06:02:30 -07001707 def GetIssueOwner(self):
1708 """Get owner from codereview, which may differ from this checkout."""
1709 return self._codereview_impl.GetIssueOwner()
1710
Edward Lemur707d70b2018-02-07 00:50:14 +01001711 def GetReviewers(self):
1712 return self._codereview_impl.GetReviewers()
1713
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001714 def GetMostRecentPatchset(self):
1715 return self._codereview_impl.GetMostRecentPatchset()
1716
tandriide281ae2016-10-12 06:02:30 -07001717 def CannotTriggerTryJobReason(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001718 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
tandriide281ae2016-10-12 06:02:30 -07001719 return self._codereview_impl.CannotTriggerTryJobReason()
1720
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001721 def GetTryJobProperties(self, patchset=None):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001722 """Returns dictionary of properties to launch tryjob."""
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001723 return self._codereview_impl.GetTryJobProperties(patchset=patchset)
tandrii8c5a3532016-11-04 07:52:02 -07001724
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001725 def __getattr__(self, attr):
1726 # This is because lots of untested code accesses Rietveld-specific stuff
1727 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001728 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001729 # Note that child method defines __getattr__ as well, and forwards it here,
1730 # because _RietveldChangelistImpl is not cleaned up yet, and given
1731 # deprecation of Rietveld, it should probably be just removed.
1732 # Until that time, avoid infinite recursion by bypassing __getattr__
1733 # of implementation class.
1734 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001735
1736
1737class _ChangelistCodereviewBase(object):
1738 """Abstract base class encapsulating codereview specifics of a changelist."""
1739 def __init__(self, changelist):
1740 self._changelist = changelist # instance of Changelist
1741
1742 def __getattr__(self, attr):
1743 # Forward methods to changelist.
1744 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1745 # _RietveldChangelistImpl to avoid this hack?
1746 return getattr(self._changelist, attr)
1747
1748 def GetStatus(self):
1749 """Apply a rough heuristic to give a simple summary of an issue's review
1750 or CQ status, assuming adherence to a common workflow.
1751
1752 Returns None if no issue for this branch, or specific string keywords.
1753 """
1754 raise NotImplementedError()
1755
1756 def GetCodereviewServer(self):
1757 """Returns server URL without end slash, like "https://codereview.com"."""
1758 raise NotImplementedError()
1759
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001760 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001761 """Fetches and returns description from the codereview server."""
1762 raise NotImplementedError()
1763
tandrii5d48c322016-08-18 16:19:37 -07001764 @classmethod
1765 def IssueConfigKey(cls):
1766 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001767 raise NotImplementedError()
1768
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001769 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001770 def PatchsetConfigKey(cls):
1771 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001772 raise NotImplementedError()
1773
tandrii5d48c322016-08-18 16:19:37 -07001774 @classmethod
1775 def CodereviewServerConfigKey(cls):
1776 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001777 raise NotImplementedError()
1778
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001779 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001780 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001781 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001782
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001783 def GetGerritObjForPresubmit(self):
1784 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1785 return None
1786
dsansomee2d6fd92016-09-08 00:10:47 -07001787 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001788 """Update the description on codereview site."""
1789 raise NotImplementedError()
1790
Aaron Gable636b13f2017-07-14 10:42:48 -07001791 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001792 """Posts a comment to the codereview site."""
1793 raise NotImplementedError()
1794
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001795 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001796 raise NotImplementedError()
1797
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001798 def CloseIssue(self):
1799 """Closes the issue."""
1800 raise NotImplementedError()
1801
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001802 def GetMostRecentPatchset(self):
1803 """Returns the most recent patchset number from the codereview site."""
1804 raise NotImplementedError()
1805
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001806 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07001807 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001808 """Fetches and applies the issue.
1809
1810 Arguments:
1811 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1812 reject: if True, reject the failed patch instead of switching to 3-way
1813 merge. Rietveld only.
1814 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1815 only.
1816 directory: switch to directory before applying the patch. Rietveld only.
Aaron Gable62619a32017-06-16 08:22:09 -07001817 force: if true, overwrites existing local state.
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001818 """
1819 raise NotImplementedError()
1820
1821 @staticmethod
1822 def ParseIssueURL(parsed_url):
1823 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1824 failed."""
1825 raise NotImplementedError()
1826
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001827 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001828 """Best effort check that user is authenticated with codereview server.
1829
1830 Arguments:
1831 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001832 refresh: whether to attempt to refresh credentials. Ignored if not
1833 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001834 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001835 raise NotImplementedError()
1836
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001837 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001838 """Best effort check that uploading isn't supposed to fail for predictable
1839 reasons.
1840
1841 This method should raise informative exception if uploading shouldn't
1842 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001843
1844 Arguments:
1845 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001846 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001847 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001848
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001849 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001850 """Uploads a change to codereview."""
1851 raise NotImplementedError()
1852
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001853 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001854 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001855
1856 Issue must have been already uploaded and known.
1857 """
1858 raise NotImplementedError()
1859
tandriie113dfd2016-10-11 10:20:12 -07001860 def CannotTriggerTryJobReason(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001861 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
tandriie113dfd2016-10-11 10:20:12 -07001862 raise NotImplementedError()
1863
tandriide281ae2016-10-12 06:02:30 -07001864 def GetIssueOwner(self):
1865 raise NotImplementedError()
1866
Edward Lemur707d70b2018-02-07 00:50:14 +01001867 def GetReviewers(self):
1868 raise NotImplementedError()
1869
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001870 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001871 raise NotImplementedError()
1872
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001873
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001874class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001875 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001876 # auth_config is Rietveld thing, kept here to preserve interface only.
1877 super(_GerritChangelistImpl, self).__init__(changelist)
1878 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001879 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001880 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001881 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001882 # Map from change number (issue) to its detail cache.
1883 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001884
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001885 if codereview_host is not None:
1886 assert not codereview_host.startswith('https://'), codereview_host
1887 self._gerrit_host = codereview_host
1888 self._gerrit_server = 'https://%s' % codereview_host
1889
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001890 def _GetGerritHost(self):
1891 # Lazy load of configs.
1892 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07001893 if self._gerrit_host and '.' not in self._gerrit_host:
1894 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
1895 # This happens for internal stuff http://crbug.com/614312.
1896 parsed = urlparse.urlparse(self.GetRemoteUrl())
1897 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001898 print('WARNING: using non-https URLs for remote is likely broken\n'
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001899 ' Your current remote is: %s' % self.GetRemoteUrl())
tandriie32e3ea2016-06-22 02:52:48 -07001900 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
1901 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001902 return self._gerrit_host
1903
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001904 def _GetGitHost(self):
1905 """Returns git host to be used when uploading change to Gerrit."""
Edward Lemur298f2cf2019-02-22 21:40:39 +00001906 remote_url = self.GetRemoteUrl()
1907 if not remote_url:
1908 return None
1909 return urlparse.urlparse(remote_url).netloc
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001910
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001911 def GetCodereviewServer(self):
1912 if not self._gerrit_server:
1913 # If we're on a branch then get the server potentially associated
1914 # with that branch.
1915 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001916 self._gerrit_server = self._GitGetBranchConfigValue(
1917 self.CodereviewServerConfigKey())
1918 if self._gerrit_server:
1919 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001920 if not self._gerrit_server:
1921 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1922 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001923 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001924 parts[0] = parts[0] + '-review'
1925 self._gerrit_host = '.'.join(parts)
1926 self._gerrit_server = 'https://%s' % self._gerrit_host
1927 return self._gerrit_server
1928
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001929 def _GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001930 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001931 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001932 if remote_url is None:
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001933 logging.warn('can\'t detect Gerrit project.')
1934 return None
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001935 project = urlparse.urlparse(remote_url).path.strip('/')
1936 if project.endswith('.git'):
1937 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00001938 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
1939 # 'a/' prefix, because 'a/' prefix is used to force authentication in
1940 # gitiles/git-over-https protocol. E.g.,
1941 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
1942 # as
1943 # https://chromium.googlesource.com/v8/v8
1944 if project.startswith('a/'):
1945 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001946 return project
1947
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001948 def _GerritChangeIdentifier(self):
1949 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
1950
1951 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001952 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001953 """
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001954 project = self._GetGerritProject()
1955 if project:
1956 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
1957 # Fall back on still unique, but less efficient change number.
1958 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001959
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001960 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001961 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001962 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001963
tandrii5d48c322016-08-18 16:19:37 -07001964 @classmethod
1965 def PatchsetConfigKey(cls):
1966 return 'gerritpatchset'
1967
1968 @classmethod
1969 def CodereviewServerConfigKey(cls):
1970 return 'gerritserver'
1971
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001972 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001973 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00001974 if settings.GetGerritSkipEnsureAuthenticated():
1975 # For projects with unusual authentication schemes.
1976 # See http://crbug.com/603378.
1977 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001978
1979 # Check presence of cookies only if using cookies-based auth method.
1980 cookie_auth = gerrit_util.Authenticator.get()
1981 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001982 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001983
Daniel Chengcf6269b2019-05-18 01:02:12 +00001984 if urlparse.urlparse(self.GetRemoteUrl()).scheme != 'https':
1985 print('WARNING: Ignoring branch %s with non-https remote %s' %
1986 (self._changelist.branch, self.GetRemoteUrl()))
1987 return
1988
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001989 # Lazy-loader to identify Gerrit and Git hosts.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001990 self.GetCodereviewServer()
1991 git_host = self._GetGitHost()
Edward Lemur298f2cf2019-02-22 21:40:39 +00001992 assert self._gerrit_server and self._gerrit_host and git_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001993
1994 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
1995 git_auth = cookie_auth.get_auth_header(git_host)
1996 if gerrit_auth and git_auth:
1997 if gerrit_auth == git_auth:
1998 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001999 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
Raul Tambre80ee78e2019-05-06 22:41:05 +00002000 print(
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002001 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002002 ' %s\n'
2003 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002004 ' Consider running the following command:\n'
2005 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002006 ' %s\n'
Raul Tambre80ee78e2019-05-06 22:41:05 +00002007 ' %s' %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002008 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002009 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002010 cookie_auth.get_new_password_message(git_host)))
2011 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002012 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002013 return
2014 else:
2015 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02002016 ([] if gerrit_auth else [self._gerrit_host]) +
2017 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002018 DieWithError('Credentials for the following hosts are required:\n'
2019 ' %s\n'
2020 'These are read from %s (or legacy %s)\n'
2021 '%s' % (
2022 '\n '.join(missing),
2023 cookie_auth.get_gitcookies_path(),
2024 cookie_auth.get_netrc_path(),
2025 cookie_auth.get_new_password_message(git_host)))
2026
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002027 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002028 if not self.GetIssue():
2029 return
2030
2031 # Warm change details cache now to avoid RPCs later, reducing latency for
2032 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002033 self._GetChangeDetail(
Andrii Shyshkalovc4a73562018-09-25 18:40:17 +00002034 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002035
2036 status = self._GetChangeDetail()['status']
2037 if status in ('MERGED', 'ABANDONED'):
2038 DieWithError('Change %s has been %s, new uploads are not allowed' %
2039 (self.GetIssueURL(),
2040 'submitted' if status == 'MERGED' else 'abandoned'))
2041
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002042 # TODO(vadimsh): For some reason the chunk of code below was skipped if
2043 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
2044 # Apparently this check is not very important? Otherwise get_auth_email
2045 # could have been added to other implementations of Authenticator.
2046 cookies_auth = gerrit_util.Authenticator.get()
2047 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002048 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002049
2050 cookies_user = cookies_auth.get_auth_email(self._GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002051 if self.GetIssueOwner() == cookies_user:
2052 return
2053 logging.debug('change %s owner is %s, cookies user is %s',
2054 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002055 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002056 # so ask what Gerrit thinks of this user.
2057 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2058 if details['email'] == self.GetIssueOwner():
2059 return
2060 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002061 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002062 'as %s.\n'
2063 'Uploading may fail due to lack of permissions.' %
2064 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2065 confirm_or_exit(action='upload')
2066
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002067 def _PostUnsetIssueProperties(self):
2068 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002069 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002070
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002071 def GetGerritObjForPresubmit(self):
2072 return presubmit_support.GerritAccessor(self._GetGerritHost())
2073
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002074 def GetStatus(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002075 """Applies a rough heuristic to give a simple summary of an issue's review
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002076 or CQ status, assuming adherence to a common workflow.
2077
2078 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002079 * 'error' - error from review tool (including deleted issues)
2080 * 'unsent' - no reviewers added
2081 * 'waiting' - waiting for review
2082 * 'reply' - waiting for uploader to reply to review
2083 * 'lgtm' - Code-Review label has been set
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00002084 * 'dry-run' - dry-running in the CQ
2085 * 'commit' - in the CQ
Aaron Gable9ab38c62017-04-06 14:36:33 -07002086 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002087 """
2088 if not self.GetIssue():
2089 return None
2090
2091 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002092 data = self._GetChangeDetail([
2093 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002094 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002095 return 'error'
2096
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002097 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002098 return 'closed'
2099
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00002100 cq_label = data['labels'].get('Commit-Queue', {})
2101 max_cq_vote = 0
2102 for vote in cq_label.get('all', []):
2103 max_cq_vote = max(max_cq_vote, vote.get('value', 0))
2104 if max_cq_vote == 2:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002105 return 'commit'
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00002106 if max_cq_vote == 1:
2107 return 'dry-run'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002108
Aaron Gable9ab38c62017-04-06 14:36:33 -07002109 if data['labels'].get('Code-Review', {}).get('approved'):
2110 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002111
2112 if not data.get('reviewers', {}).get('REVIEWER', []):
2113 return 'unsent'
2114
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002115 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002116 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2117 last_message_author = messages.pop().get('author', {})
2118 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002119 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2120 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002121 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002122 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002123 if last_message_author.get('_account_id') == owner:
2124 # Most recent message was by owner.
2125 return 'waiting'
2126 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002127 # Some reply from non-owner.
2128 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002129
2130 # Somehow there are no messages even though there are reviewers.
2131 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002132
2133 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002134 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002135 patchset = data['revisions'][data['current_revision']]['_number']
2136 self.SetPatchset(patchset)
2137 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002138
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002139 def FetchDescription(self, force=False):
2140 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2141 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002142 current_rev = data['current_revision']
Dan Beamcf6df902018-11-08 01:48:37 +00002143 return data['revisions'][current_rev]['commit']['message'].encode(
2144 'utf-8', 'ignore')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002145
dsansomee2d6fd92016-09-08 00:10:47 -07002146 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002147 if gerrit_util.HasPendingChangeEdit(
2148 self._GetGerritHost(), self._GerritChangeIdentifier()):
dsansomee2d6fd92016-09-08 00:10:47 -07002149 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002150 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002151 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002152 'unpublished edit. Either publish the edit in the Gerrit web UI '
2153 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002154
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002155 gerrit_util.DeletePendingChangeEdit(
2156 self._GetGerritHost(), self._GerritChangeIdentifier())
2157 gerrit_util.SetCommitMessage(
2158 self._GetGerritHost(), self._GerritChangeIdentifier(),
2159 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002160
Aaron Gable636b13f2017-07-14 10:42:48 -07002161 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002162 gerrit_util.SetReview(
2163 self._GetGerritHost(), self._GerritChangeIdentifier(),
2164 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002165
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002166 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002167 # DETAILED_ACCOUNTS is to get emails in accounts.
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002168 # CURRENT_REVISION is included to get the latest patchset so that
2169 # only the robot comments from the latest patchset can be shown.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002170 messages = self._GetChangeDetail(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002171 options=['MESSAGES', 'DETAILED_ACCOUNTS',
2172 'CURRENT_REVISION']).get('messages', [])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002173 file_comments = gerrit_util.GetChangeComments(
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002174 self._GetGerritHost(), self._GerritChangeIdentifier())
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002175 robot_file_comments = gerrit_util.GetChangeRobotComments(
2176 self._GetGerritHost(), self._GerritChangeIdentifier())
2177
2178 # Add the robot comments onto the list of comments, but only
2179 # keep those that are from the latest pachset.
2180 latest_patch_set = self.GetMostRecentPatchset()
2181 for path, robot_comments in robot_file_comments.iteritems():
2182 line_comments = file_comments.setdefault(path, [])
2183 line_comments.extend(
2184 [c for c in robot_comments if c['patch_set'] == latest_patch_set])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002185
2186 # Build dictionary of file comments for easy access and sorting later.
2187 # {author+date: {path: {patchset: {line: url+message}}}}
2188 comments = collections.defaultdict(
2189 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2190 for path, line_comments in file_comments.iteritems():
2191 for comment in line_comments:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002192 tag = comment.get('tag', '')
2193 if tag.startswith('autogenerated') and 'robot_id' not in comment:
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002194 continue
2195 key = (comment['author']['email'], comment['updated'])
2196 if comment.get('side', 'REVISION') == 'PARENT':
2197 patchset = 'Base'
2198 else:
2199 patchset = 'PS%d' % comment['patch_set']
2200 line = comment.get('line', 0)
2201 url = ('https://%s/c/%s/%s/%s#%s%s' %
2202 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2203 'b' if comment.get('side') == 'PARENT' else '',
2204 str(line) if line else ''))
2205 comments[key][path][patchset][line] = (url, comment['message'])
2206
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002207 summaries = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002208 for msg in messages:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002209 summary = self._BuildCommentSummary(msg, comments, readable)
2210 if summary:
2211 summaries.append(summary)
2212 return summaries
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002213
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002214 @staticmethod
2215 def _BuildCommentSummary(msg, comments, readable):
2216 key = (msg['author']['email'], msg['date'])
2217 # Don't bother showing autogenerated messages that don't have associated
2218 # file or line comments. this will filter out most autogenerated
2219 # messages, but will keep robot comments like those from Tricium.
2220 is_autogenerated = msg.get('tag', '').startswith('autogenerated')
2221 if is_autogenerated and not comments.get(key):
2222 return None
2223 message = msg['message']
2224 # Gerrit spits out nanoseconds.
2225 assert len(msg['date'].split('.')[-1]) == 9
2226 date = datetime.datetime.strptime(msg['date'][:-3],
2227 '%Y-%m-%d %H:%M:%S.%f')
2228 if key in comments:
2229 message += '\n'
2230 for path, patchsets in sorted(comments.get(key, {}).items()):
2231 if readable:
2232 message += '\n%s' % path
2233 for patchset, lines in sorted(patchsets.items()):
2234 for line, (url, content) in sorted(lines.items()):
2235 if line:
2236 line_str = 'Line %d' % line
2237 path_str = '%s:%d:' % (path, line)
2238 else:
2239 line_str = 'File comment'
2240 path_str = '%s:0:' % path
2241 if readable:
2242 message += '\n %s, %s: %s' % (patchset, line_str, url)
2243 message += '\n %s\n' % content
2244 else:
2245 message += '\n%s ' % path_str
2246 message += '\n%s\n' % content
2247
2248 return _CommentSummary(
2249 date=date,
2250 message=message,
2251 sender=msg['author']['email'],
2252 autogenerated=is_autogenerated,
2253 # These could be inferred from the text messages and correlated with
2254 # Code-Review label maximum, however this is not reliable.
2255 # Leaving as is until the need arises.
2256 approval=False,
2257 disapproval=False,
2258 )
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002259
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002260 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002261 gerrit_util.AbandonChange(
2262 self._GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002263
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002264 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002265 gerrit_util.SubmitChange(
2266 self._GetGerritHost(), self._GerritChangeIdentifier(),
2267 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002268
Andrii Shyshkalovb7214602018-08-22 23:20:26 +00002269 def _GetChangeDetail(self, options=None, no_cache=False):
2270 """Returns details of associated Gerrit change and caching results.
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002271
2272 If fresh data is needed, set no_cache=True which will clear cache and
2273 thus new data will be fetched from Gerrit.
2274 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002275 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002276 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002277
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002278 # Optimization to avoid multiple RPCs:
2279 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2280 'CURRENT_COMMIT' not in options):
2281 options.append('CURRENT_COMMIT')
2282
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002283 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002284 cache_key = str(self.GetIssue())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002285 options = [o.upper() for o in options]
2286
2287 # Check in cache first unless no_cache is True.
2288 if no_cache:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002289 self._detail_cache.pop(cache_key, None)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002290 else:
2291 options_set = frozenset(options)
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002292 for cached_options_set, data in self._detail_cache.get(cache_key, []):
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002293 # Assumption: data fetched before with extra options is suitable
2294 # for return for a smaller set of options.
2295 # For example, if we cached data for
2296 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2297 # and request is for options=[CURRENT_REVISION],
2298 # THEN we can return prior cached data.
2299 if options_set.issubset(cached_options_set):
2300 return data
2301
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002302 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002303 data = gerrit_util.GetChangeDetail(
2304 self._GetGerritHost(), self._GerritChangeIdentifier(), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002305 except gerrit_util.GerritError as e:
2306 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002307 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002308 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002309
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002310 self._detail_cache.setdefault(cache_key, []).append(
2311 (frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002312 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002313
Andrii Shyshkalovcc5f17e2018-08-22 23:35:59 +00002314 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002315 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002316 try:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002317 data = gerrit_util.GetChangeCommit(
2318 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002319 except gerrit_util.GerritError as e:
2320 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002321 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002322 raise
agable32978d92016-11-01 12:55:02 -07002323 return data
2324
Karen Qian40c19422019-03-13 21:28:29 +00002325 def _IsCqConfigured(self):
2326 detail = self._GetChangeDetail(['LABELS'])
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002327 if u'Commit-Queue' not in detail.get('labels', {}):
Karen Qian40c19422019-03-13 21:28:29 +00002328 return False
2329 # TODO(crbug/753213): Remove temporary hack
2330 if ('https://chromium.googlesource.com/chromium/src' ==
2331 self._changelist.GetRemoteUrl() and
2332 detail['branch'].startswith('refs/branch-heads/')):
2333 return False
2334 return True
2335
Olivier Robin75ee7252018-04-13 10:02:56 +02002336 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002337 if git_common.is_dirty_git_tree('land'):
2338 return 1
Karen Qian40c19422019-03-13 21:28:29 +00002339
tandriid60367b2016-06-22 05:25:12 -07002340 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
Karen Qian40c19422019-03-13 21:28:29 +00002341 if not force and self._IsCqConfigured():
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00002342 confirm_or_exit('\nIt seems this repository has a CQ, '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002343 'which can test and land changes for you. '
2344 'Are you sure you wish to bypass it?\n',
2345 action='bypass CQ')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002346 differs = True
tandriic4344b52016-08-29 06:04:54 -07002347 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002348 # Note: git diff outputs nothing if there is no diff.
2349 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002350 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002351 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002352 if detail['current_revision'] == last_upload:
2353 differs = False
2354 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002355 print('WARNING: Local branch contents differ from latest uploaded '
2356 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002357 if differs:
2358 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002359 confirm_or_exit(
2360 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2361 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002362 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002363 elif not bypass_hooks:
2364 hook_results = self.RunHook(
2365 committing=True,
2366 may_prompt=not force,
2367 verbose=verbose,
Olivier Robin75ee7252018-04-13 10:02:56 +02002368 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None),
2369 parallel=parallel)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002370 if not hook_results.should_continue():
2371 return 1
2372
2373 self.SubmitIssue(wait_for_merge=True)
2374 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002375 links = self._GetChangeCommit().get('web_links', [])
2376 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002377 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002378 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002379 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002380 return 0
2381
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002382 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002383 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002384 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002385 assert not directory
2386 assert parsed_issue_arg.valid
2387
2388 self._changelist.issue = parsed_issue_arg.issue
2389
2390 if parsed_issue_arg.hostname:
2391 self._gerrit_host = parsed_issue_arg.hostname
2392 self._gerrit_server = 'https://%s' % self._gerrit_host
2393
tandriic2405f52016-10-10 08:13:15 -07002394 try:
2395 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002396 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002397 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002398
2399 if not parsed_issue_arg.patchset:
2400 # Use current revision by default.
2401 revision_info = detail['revisions'][detail['current_revision']]
2402 patchset = int(revision_info['_number'])
2403 else:
2404 patchset = parsed_issue_arg.patchset
2405 for revision_info in detail['revisions'].itervalues():
2406 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2407 break
2408 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002409 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002410 (parsed_issue_arg.patchset, self.GetIssue()))
2411
Aaron Gable697a91b2018-01-19 15:20:15 -08002412 remote_url = self._changelist.GetRemoteUrl()
2413 if remote_url.endswith('.git'):
2414 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002415 remote_url = remote_url.rstrip('/')
2416
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002417 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002418 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002419
2420 if remote_url != fetch_info['url']:
2421 DieWithError('Trying to patch a change from %s but this repo appears '
2422 'to be %s.' % (fetch_info['url'], remote_url))
2423
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002424 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002425
Aaron Gable62619a32017-06-16 08:22:09 -07002426 if force:
2427 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2428 print('Checked out commit for change %i patchset %i locally' %
2429 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002430 elif nocommit:
2431 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2432 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002433 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002434 RunGit(['cherry-pick', 'FETCH_HEAD'])
2435 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002436 (parsed_issue_arg.issue, patchset))
2437 print('Note: this created a local commit which does not have '
2438 'the same hash as the one uploaded for review. This will make '
2439 'uploading changes based on top of this branch difficult.\n'
2440 'If you want to do that, use "git cl patch --force" instead.')
2441
Stefan Zagerd08043c2017-10-12 12:07:02 -07002442 if self.GetBranch():
2443 self.SetIssue(parsed_issue_arg.issue)
2444 self.SetPatchset(patchset)
2445 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2446 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2447 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2448 else:
2449 print('WARNING: You are in detached HEAD state.\n'
2450 'The patch has been applied to your checkout, but you will not be '
2451 'able to upload a new patch set to the gerrit issue.\n'
2452 'Try using the \'-b\' option if you would like to work on a '
2453 'branch and/or upload a new patch set.')
2454
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002455 return 0
2456
2457 @staticmethod
2458 def ParseIssueURL(parsed_url):
2459 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2460 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002461 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2462 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002463 # Short urls like https://domain/<issue_number> can be used, but don't allow
2464 # specifying the patchset (you'd 404), but we allow that here.
2465 if parsed_url.path == '/':
2466 part = parsed_url.fragment
2467 else:
2468 part = parsed_url.path
Bruce Dawson9c062012019-05-02 19:20:28 +00002469 match = re.match(r'(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002470 if match:
2471 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002472 issue=int(match.group(3)),
2473 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002474 hostname=parsed_url.netloc,
2475 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002476 return None
2477
tandrii16e0b4e2016-06-07 10:34:28 -07002478 def _GerritCommitMsgHookCheck(self, offer_removal):
2479 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2480 if not os.path.exists(hook):
2481 return
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002482 # Crude attempt to distinguish Gerrit Codereview hook from a potentially
2483 # custom developer-made one.
tandrii16e0b4e2016-06-07 10:34:28 -07002484 data = gclient_utils.FileRead(hook)
2485 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2486 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002487 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002488 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002489 'and may interfere with it in subtle ways.\n'
2490 'We recommend you remove the commit-msg hook.')
2491 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002492 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002493 gclient_utils.rm_file_or_tree(hook)
2494 print('Gerrit commit-msg hook removed.')
2495 else:
2496 print('OK, will keep Gerrit commit-msg hook in place.')
2497
Edward Lemur1b52d872019-05-09 21:12:12 +00002498 def _CleanUpOldTraces(self):
2499 """Keep only the last |MAX_TRACES| traces."""
2500 try:
2501 traces = sorted([
2502 os.path.join(TRACES_DIR, f)
2503 for f in os.listdir(TRACES_DIR)
2504 if (os.path.isfile(os.path.join(TRACES_DIR, f))
2505 and not f.startswith('tmp'))
2506 ])
2507 traces_to_delete = traces[:-MAX_TRACES]
2508 for trace in traces_to_delete:
Daniel Chengcf6269b2019-05-18 01:02:12 +00002509 os.remove(trace)
Edward Lemur1b52d872019-05-09 21:12:12 +00002510 except OSError:
2511 print('WARNING: Failed to remove old git traces from\n'
2512 ' %s'
2513 'Consider removing them manually.' % TRACES_DIR)
Edward Lemurdc8e23d2019-05-07 00:45:48 +00002514
Edward Lemur5737f022019-05-17 01:24:00 +00002515 def _WriteGitPushTraces(self, trace_name, traces_dir, git_push_metadata):
Edward Lemur1b52d872019-05-09 21:12:12 +00002516 """Zip and write the git push traces stored in traces_dir."""
2517 gclient_utils.safe_makedirs(TRACES_DIR)
Edward Lemur1b52d872019-05-09 21:12:12 +00002518 traces_zip = trace_name + '-traces'
2519 traces_readme = trace_name + '-README'
Michael Mosse7f0b4c2019-05-08 04:36:24 +00002520 # Create a temporary dir to store git config and gitcookies in. It will be
2521 # compressed and stored next to the traces.
2522 git_info_dir = tempfile.mkdtemp()
Edward Lemur1b52d872019-05-09 21:12:12 +00002523 git_info_zip = trace_name + '-git-info'
2524
Edward Lemur5737f022019-05-17 01:24:00 +00002525 git_push_metadata['now'] = datetime_now().strftime('%c')
Eric Boren67c48202019-05-30 16:52:51 +00002526 if sys.stdin.encoding and sys.stdin.encoding != 'utf-8':
sangwoo.ko7a614332019-05-22 02:46:19 +00002527 git_push_metadata['now'] = git_push_metadata['now'].decode(
2528 sys.stdin.encoding)
2529
Edward Lemur1b52d872019-05-09 21:12:12 +00002530 git_push_metadata['trace_name'] = trace_name
2531 gclient_utils.FileWrite(
2532 traces_readme, TRACES_README_FORMAT % git_push_metadata)
2533
2534 # Keep only the first 6 characters of the git hashes on the packet
2535 # trace. This greatly decreases size after compression.
2536 packet_traces = os.path.join(traces_dir, 'trace-packet')
2537 if os.path.isfile(packet_traces):
2538 contents = gclient_utils.FileRead(packet_traces)
2539 gclient_utils.FileWrite(
2540 packet_traces, GIT_HASH_RE.sub(r'\1', contents))
2541 shutil.make_archive(traces_zip, 'zip', traces_dir)
2542
2543 # Collect and compress the git config and gitcookies.
2544 git_config = RunGit(['config', '-l'])
2545 gclient_utils.FileWrite(
2546 os.path.join(git_info_dir, 'git-config'),
2547 git_config)
2548
2549 cookie_auth = gerrit_util.Authenticator.get()
2550 if isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
2551 gitcookies_path = cookie_auth.get_gitcookies_path()
2552 if os.path.isfile(gitcookies_path):
2553 gitcookies = gclient_utils.FileRead(gitcookies_path)
2554 gclient_utils.FileWrite(
2555 os.path.join(git_info_dir, 'gitcookies'),
2556 GITCOOKIES_REDACT_RE.sub('REDACTED', gitcookies))
2557 shutil.make_archive(git_info_zip, 'zip', git_info_dir)
2558
Edward Lemur1b52d872019-05-09 21:12:12 +00002559 gclient_utils.rmtree(git_info_dir)
2560
2561 def _RunGitPushWithTraces(
2562 self, change_desc, refspec, refspec_opts, git_push_metadata):
2563 """Run git push and collect the traces resulting from the execution."""
2564 # Create a temporary directory to store traces in. Traces will be compressed
2565 # and stored in a 'traces' dir inside depot_tools.
2566 traces_dir = tempfile.mkdtemp()
Edward Lemur5737f022019-05-17 01:24:00 +00002567 trace_name = os.path.join(
2568 TRACES_DIR, datetime_now().strftime('%Y%m%dT%H%M%S.%f'))
Edward Lemur0f58ae42019-04-30 17:24:12 +00002569
2570 env = os.environ.copy()
2571 env['GIT_REDACT_COOKIES'] = 'o,SSO,GSSO_Uberproxy'
2572 env['GIT_TR2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Jonathan Nieder9779b142019-05-29 23:19:29 +00002573 env['GIT_TRACE2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Edward Lemur0f58ae42019-04-30 17:24:12 +00002574 env['GIT_TRACE_CURL'] = os.path.join(traces_dir, 'trace-curl')
2575 env['GIT_TRACE_CURL_NO_DATA'] = '1'
2576 env['GIT_TRACE_PACKET'] = os.path.join(traces_dir, 'trace-packet')
2577
2578 try:
2579 push_returncode = 0
Edward Lemur1b52d872019-05-09 21:12:12 +00002580 remote_url = self.GetRemoteUrl()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002581 before_push = time_time()
2582 push_stdout = gclient_utils.CheckCallAndFilter(
Edward Lemur1b52d872019-05-09 21:12:12 +00002583 ['git', 'push', remote_url, refspec],
Edward Lemur0f58ae42019-04-30 17:24:12 +00002584 env=env,
2585 print_stdout=True,
2586 # Flush after every line: useful for seeing progress when running as
2587 # recipe.
2588 filter_fn=lambda _: sys.stdout.flush())
2589 except subprocess2.CalledProcessError as e:
2590 push_returncode = e.returncode
2591 DieWithError('Failed to create a change. Please examine output above '
2592 'for the reason of the failure.\n'
2593 'Hint: run command below to diagnose common Git/Gerrit '
2594 'credential problems:\n'
Edward Lemur5737f022019-05-17 01:24:00 +00002595 ' git cl creds-check\n'
2596 '\n'
2597 'If git-cl is not working correctly, file a bug under the '
2598 'Infra>SDK component including the files below.\n'
2599 'Review the files before upload, since they might contain '
2600 'sensitive information.\n'
2601 'Set the Restrict-View-Google label so that they are not '
2602 'publicly accessible.\n'
2603 + TRACES_MESSAGE % {'trace_name': trace_name},
Edward Lemur0f58ae42019-04-30 17:24:12 +00002604 change_desc)
2605 finally:
2606 execution_time = time_time() - before_push
2607 metrics.collector.add_repeated('sub_commands', {
2608 'command': 'git push',
2609 'execution_time': execution_time,
2610 'exit_code': push_returncode,
2611 'arguments': metrics_utils.extract_known_subcommand_args(refspec_opts),
2612 })
2613
Edward Lemur1b52d872019-05-09 21:12:12 +00002614 git_push_metadata['execution_time'] = execution_time
2615 git_push_metadata['exit_code'] = push_returncode
Edward Lemur5737f022019-05-17 01:24:00 +00002616 self._WriteGitPushTraces(trace_name, traces_dir, git_push_metadata)
Edward Lemur0f58ae42019-04-30 17:24:12 +00002617
Edward Lemur1b52d872019-05-09 21:12:12 +00002618 self._CleanUpOldTraces()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002619 gclient_utils.rmtree(traces_dir)
2620
2621 return push_stdout
2622
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002623 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002624 """Upload the current branch to Gerrit."""
Mike Frysingera989d552019-08-14 20:51:23 +00002625 if options.squash is None:
tandriia60502f2016-06-20 02:01:53 -07002626 # Load default for user, repo, squash=true, in this order.
2627 options.squash = settings.GetSquashGerritUploads()
tandrii26f3e4e2016-06-10 08:37:04 -07002628
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002629 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002630 branch = GetTargetRef(remote, remote_branch, options.target_branch)
Aaron Gableb56ad332017-01-06 15:24:31 -08002631 # This may be None; default fallback value is determined in logic below.
2632 title = options.title
2633
Dominic Battre7d1c4842017-10-27 09:17:28 +02002634 # Extract bug number from branch name.
2635 bug = options.bug
2636 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2637 if not bug and match:
2638 bug = match.group(1)
2639
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002640 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002641 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002642 if self.GetIssue():
2643 # Try to get the message from a previous upload.
2644 message = self.GetDescription()
2645 if not message:
2646 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002647 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002648 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002649 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002650 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002651 # When uploading a subsequent patchset, -m|--message is taken
2652 # as the patchset title if --title was not provided.
2653 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002654 else:
2655 default_title = RunGit(
2656 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002657 if options.force:
2658 title = default_title
2659 else:
2660 title = ask_for_data(
2661 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002662 change_id = self._GetChangeDetail()['change_id']
2663 while True:
2664 footer_change_ids = git_footers.get_footer_change_id(message)
2665 if footer_change_ids == [change_id]:
2666 break
2667 if not footer_change_ids:
2668 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002669 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002670 continue
2671 # There is already a valid footer but with different or several ids.
2672 # Doing this automatically is non-trivial as we don't want to lose
2673 # existing other footers, yet we want to append just 1 desired
2674 # Change-Id. Thus, just create a new footer, but let user verify the
2675 # new description.
2676 message = '%s\n\nChange-Id: %s' % (message, change_id)
2677 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002678 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002679 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002680 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002681 'Please, check the proposed correction to the description, '
2682 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2683 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2684 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002685 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002686 if not options.force:
2687 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002688 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002689 message = change_desc.description
2690 if not message:
2691 DieWithError("Description is empty. Aborting...")
2692 # Continue the while loop.
2693 # Sanity check of this code - we should end up with proper message
2694 # footer.
2695 assert [change_id] == git_footers.get_footer_change_id(message)
2696 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002697 else: # if not self.GetIssue()
2698 if options.message:
2699 message = options.message
2700 else:
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002701 message = _create_description_from_log(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002702 if options.title:
2703 message = options.title + '\n\n' + message
2704 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002705
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002706 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02002707 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002708 # On first upload, patchset title is always this string, while
2709 # --title flag gets converted to first line of message.
2710 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002711 if not change_desc.description:
2712 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002713 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002714 if len(change_ids) > 1:
2715 DieWithError('too many Change-Id footers, at most 1 allowed.')
2716 if not change_ids:
2717 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002718 change_desc.set_description(git_footers.add_footer_change_id(
2719 change_desc.description,
2720 GenerateGerritChangeId(change_desc.description)))
2721 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002722 assert len(change_ids) == 1
2723 change_id = change_ids[0]
2724
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002725 if options.reviewers or options.tbrs or options.add_owners_to:
2726 change_desc.update_reviewers(options.reviewers, options.tbrs,
2727 options.add_owners_to, change)
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00002728 if options.preserve_tryjobs:
2729 change_desc.set_preserve_tryjobs()
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002730
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002731 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002732 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2733 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002734 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07002735 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
2736 desc_tempfile.write(change_desc.description)
2737 desc_tempfile.close()
2738 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2739 '-F', desc_tempfile.name]).strip()
2740 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002741 else:
2742 change_desc = ChangeDescription(
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002743 options.message or _create_description_from_log(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002744 if not change_desc.description:
2745 DieWithError("Description is empty. Aborting...")
2746
2747 if not git_footers.get_footer_change_id(change_desc.description):
2748 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002749 change_desc.set_description(
2750 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002751 if options.reviewers or options.tbrs or options.add_owners_to:
2752 change_desc.update_reviewers(options.reviewers, options.tbrs,
2753 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002754 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002755 # For no-squash mode, we assume the remote called "origin" is the one we
2756 # want. It is not worthwhile to support different workflows for
2757 # no-squash mode.
2758 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002759 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2760
2761 assert change_desc
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00002762 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002763 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2764 ref_to_push)]).splitlines()
2765 if len(commits) > 1:
2766 print('WARNING: This will upload %d commits. Run the following command '
2767 'to see which commits will be uploaded: ' % len(commits))
2768 print('git log %s..%s' % (parent, ref_to_push))
2769 print('You can also use `git squash-branch` to squash these into a '
2770 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002771 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002772
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002773 if options.reviewers or options.tbrs or options.add_owners_to:
2774 change_desc.update_reviewers(options.reviewers, options.tbrs,
2775 options.add_owners_to, change)
2776
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002777 reviewers = sorted(change_desc.get_reviewers())
2778 # Add cc's from the CC_LIST and --cc flag (if any).
2779 if not options.private and not options.no_autocc:
2780 cc = self.GetCCList().split(',')
2781 else:
2782 cc = []
2783 if options.cc:
2784 cc.extend(options.cc)
2785 cc = filter(None, [email.strip() for email in cc])
2786 if change_desc.get_cced():
2787 cc.extend(change_desc.get_cced())
Andrii Shyshkalov0da5e8f2018-10-30 17:29:18 +00002788 if self._GetGerritHost() == 'chromium-review.googlesource.com':
2789 valid_accounts = set(reviewers + cc)
2790 # TODO(crbug/877717): relax this for all hosts.
2791 else:
2792 valid_accounts = gerrit_util.ValidAccounts(
2793 self._GetGerritHost(), reviewers + cc)
Andrii Shyshkalovf170af42018-10-30 07:00:44 +00002794 logging.info('accounts %s are recognized, %s invalid',
2795 sorted(valid_accounts),
2796 set(reviewers + cc).difference(set(valid_accounts)))
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002797
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002798 # Extra options that can be specified at push time. Doc:
2799 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002800 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002801
Aaron Gable844cf292017-06-28 11:32:59 -07002802 # By default, new changes are started in WIP mode, and subsequent patchsets
2803 # don't send email. At any time, passing --send-mail will mark the change
2804 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07002805 if options.send_mail:
2806 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07002807 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04002808 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002809 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07002810 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002811 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07002812
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002813 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07002814 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002815
Aaron Gable9b713dd2016-12-14 16:04:21 -08002816 if title:
Nick Carter8692b182017-11-06 16:30:38 -08002817 # Punctuation and whitespace in |title| must be percent-encoded.
2818 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002819
agablec6787972016-09-09 16:13:34 -07002820 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07002821 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07002822
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002823 for r in sorted(reviewers):
2824 if r in valid_accounts:
2825 refspec_opts.append('r=%s' % r)
2826 reviewers.remove(r)
2827 else:
2828 # TODO(tandrii): this should probably be a hard failure.
2829 print('WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
2830 % r)
2831 for c in sorted(cc):
2832 # refspec option will be rejected if cc doesn't correspond to an
2833 # account, even though REST call to add such arbitrary cc may succeed.
2834 if c in valid_accounts:
2835 refspec_opts.append('cc=%s' % c)
2836 cc.remove(c)
2837
rmistry9eadede2016-09-19 11:22:43 -07002838 if options.topic:
2839 # Documentation on Gerrit topics is here:
2840 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002841 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002842
Edward Lemur687ca902018-12-05 02:30:30 +00002843 if options.enable_auto_submit:
2844 refspec_opts.append('l=Auto-Submit+1')
2845 if options.use_commit_queue:
2846 refspec_opts.append('l=Commit-Queue+2')
2847 elif options.cq_dry_run:
2848 refspec_opts.append('l=Commit-Queue+1')
2849
2850 if change_desc.get_reviewers(tbr_only=True):
2851 score = gerrit_util.GetCodeReviewTbrScore(
2852 self._GetGerritHost(),
2853 self._GetGerritProject())
2854 refspec_opts.append('l=Code-Review+%s' % score)
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00002855
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002856 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08002857 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002858 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08002859 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002860 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
2861
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002862 refspec_suffix = ''
2863 if refspec_opts:
2864 refspec_suffix = '%' + ','.join(refspec_opts)
2865 assert ' ' not in refspec_suffix, (
2866 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2867 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2868
Edward Lemur1b52d872019-05-09 21:12:12 +00002869 git_push_metadata = {
2870 'gerrit_host': self._GetGerritHost(),
2871 'title': title or '<untitled>',
2872 'change_id': change_id,
2873 'description': change_desc.description,
2874 }
2875 push_stdout = self._RunGitPushWithTraces(
2876 change_desc, refspec, refspec_opts, git_push_metadata)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002877
2878 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07002879 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002880 change_numbers = [m.group(1)
2881 for m in map(regex.match, push_stdout.splitlines())
2882 if m]
2883 if len(change_numbers) != 1:
2884 DieWithError(
2885 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002886 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002887 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002888 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002889
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002890 if self.GetIssue() and (reviewers or cc):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002891 # GetIssue() is not set in case of non-squash uploads according to tests.
2892 # TODO(agable): non-squash uploads in git cl should be removed.
2893 gerrit_util.AddReviewers(
2894 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002895 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002896 reviewers, cc,
2897 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002898
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002899 return 0
2900
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002901 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
2902 change_desc):
2903 """Computes parent of the generated commit to be uploaded to Gerrit.
2904
2905 Returns revision or a ref name.
2906 """
2907 if custom_cl_base:
2908 # Try to avoid creating additional unintended CLs when uploading, unless
2909 # user wants to take this risk.
2910 local_ref_of_target_remote = self.GetRemoteBranch()[1]
2911 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
2912 local_ref_of_target_remote])
2913 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002914 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002915 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
2916 'If you proceed with upload, more than 1 CL may be created by '
2917 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
2918 'If you are certain that specified base `%s` has already been '
2919 'uploaded to Gerrit as another CL, you may proceed.\n' %
2920 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
2921 if not force:
2922 confirm_or_exit(
2923 'Do you take responsibility for cleaning up potential mess '
2924 'resulting from proceeding with upload?',
2925 action='upload')
2926 return custom_cl_base
2927
Aaron Gablef97e33d2017-03-30 15:44:27 -07002928 if remote != '.':
2929 return self.GetCommonAncestorWithUpstream()
2930
2931 # If our upstream branch is local, we base our squashed commit on its
2932 # squashed version.
2933 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2934
Aaron Gablef97e33d2017-03-30 15:44:27 -07002935 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07002936 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07002937
2938 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002939 # TODO(tandrii): consider checking parent change in Gerrit and using its
2940 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
2941 # the tree hash of the parent branch. The upside is less likely bogus
2942 # requests to reupload parent change just because it's uploadhash is
2943 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07002944 parent = RunGit(['config',
2945 'branch.%s.gerritsquashhash' % upstream_branch_name],
2946 error_ok=True).strip()
2947 # Verify that the upstream branch has been uploaded too, otherwise
2948 # Gerrit will create additional CLs when uploading.
2949 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2950 RunGitSilent(['rev-parse', parent + ':'])):
2951 DieWithError(
2952 '\nUpload upstream branch %s first.\n'
2953 'It is likely that this branch has been rebased since its last '
2954 'upload, so you just need to upload it again.\n'
2955 '(If you uploaded it with --no-squash, then branch dependencies '
2956 'are not supported, and you should reupload with --squash.)'
2957 % upstream_branch_name,
2958 change_desc)
2959 return parent
2960
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002961 def _AddChangeIdToCommitMessage(self, options, args):
2962 """Re-commits using the current message, assumes the commit hook is in
2963 place.
2964 """
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002965 log_desc = options.message or _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002966 git_command = ['commit', '--amend', '-m', log_desc]
2967 RunGit(git_command)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002968 new_log_desc = _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002969 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002970 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002971 return new_log_desc
2972 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002973 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002974
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002975 def SetCQState(self, new_state):
2976 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002977 vote_map = {
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002978 _CQState.NONE: 0,
2979 _CQState.DRY_RUN: 1,
2980 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002981 }
Aaron Gablefc62f762017-07-17 11:12:07 -07002982 labels = {'Commit-Queue': vote_map[new_state]}
2983 notify = False if new_state == _CQState.DRY_RUN else None
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002984 gerrit_util.SetReview(
2985 self._GetGerritHost(), self._GerritChangeIdentifier(),
2986 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002987
tandriie113dfd2016-10-11 10:20:12 -07002988 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002989 try:
2990 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002991 except GerritChangeNotExists:
2992 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002993
2994 if data['status'] in ('ABANDONED', 'MERGED'):
2995 return 'CL %s is closed' % self.GetIssue()
2996
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002997 def GetTryJobProperties(self, patchset=None):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002998 """Returns dictionary of properties to launch a tryjob."""
tandrii8c5a3532016-11-04 07:52:02 -07002999 data = self._GetChangeDetail(['ALL_REVISIONS'])
3000 patchset = int(patchset or self.GetPatchset())
3001 assert patchset
3002 revision_data = None # Pylint wants it to be defined.
3003 for revision_data in data['revisions'].itervalues():
3004 if int(revision_data['_number']) == patchset:
3005 break
3006 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003007 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003008 (patchset, self.GetIssue()))
3009 return {
3010 'patch_issue': self.GetIssue(),
3011 'patch_set': patchset or self.GetPatchset(),
3012 'patch_project': data['project'],
3013 'patch_storage': 'gerrit',
3014 'patch_ref': revision_data['fetch']['http']['ref'],
3015 'patch_repository_url': revision_data['fetch']['http']['url'],
3016 'patch_gerrit_url': self.GetCodereviewServer(),
3017 }
tandriie113dfd2016-10-11 10:20:12 -07003018
tandriide281ae2016-10-12 06:02:30 -07003019 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003020 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003021
Edward Lemur707d70b2018-02-07 00:50:14 +01003022 def GetReviewers(self):
3023 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
Mohamed Heikal171c0742018-11-09 20:38:51 +00003024 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01003025
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003026
3027_CODEREVIEW_IMPLEMENTATIONS = {
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003028 'gerrit': _GerritChangelistImpl,
3029}
3030
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003031
iannuccie53c9352016-08-17 14:40:40 -07003032def _add_codereview_issue_select_options(parser, extra=""):
3033 _add_codereview_select_options(parser)
3034
3035 text = ('Operate on this issue number instead of the current branch\'s '
3036 'implicit issue.')
3037 if extra:
3038 text += ' '+extra
3039 parser.add_option('-i', '--issue', type=int, help=text)
3040
3041
3042def _process_codereview_issue_select_options(parser, options):
3043 _process_codereview_select_options(parser, options)
3044 if options.issue is not None and not options.forced_codereview:
3045 parser.error('--issue must be specified with either --rietveld or --gerrit')
3046
3047
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003048def _add_codereview_select_options(parser):
3049 """Appends --gerrit and --rietveld options to force specific codereview."""
3050 parser.codereview_group = optparse.OptionGroup(
3051 parser, 'EXPERIMENTAL! Codereview override options')
3052 parser.add_option_group(parser.codereview_group)
3053 parser.codereview_group.add_option(
3054 '--gerrit', action='store_true',
3055 help='Force the use of Gerrit for codereview')
3056 parser.codereview_group.add_option(
3057 '--rietveld', action='store_true',
3058 help='Force the use of Rietveld for codereview')
3059
3060
3061def _process_codereview_select_options(parser, options):
Andrii Shyshkalovfeec80e2018-10-16 01:00:47 +00003062 if options.rietveld:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00003063 parser.error('--rietveld is no longer supported.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003064 options.forced_codereview = None
3065 if options.gerrit:
3066 options.forced_codereview = 'gerrit'
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003067
3068
tandriif9aefb72016-07-01 09:06:51 -07003069def _get_bug_line_values(default_project, bugs):
3070 """Given default_project and comma separated list of bugs, yields bug line
3071 values.
3072
3073 Each bug can be either:
3074 * a number, which is combined with default_project
3075 * string, which is left as is.
3076
3077 This function may produce more than one line, because bugdroid expects one
3078 project per line.
3079
3080 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3081 ['v8:123', 'chromium:789']
3082 """
3083 default_bugs = []
3084 others = []
3085 for bug in bugs.split(','):
3086 bug = bug.strip()
3087 if bug:
3088 try:
3089 default_bugs.append(int(bug))
3090 except ValueError:
3091 others.append(bug)
3092
3093 if default_bugs:
3094 default_bugs = ','.join(map(str, default_bugs))
3095 if default_project:
3096 yield '%s:%s' % (default_project, default_bugs)
3097 else:
3098 yield default_bugs
3099 for other in sorted(others):
3100 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3101 yield other
3102
3103
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003104class ChangeDescription(object):
3105 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003106 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003107 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003108 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003109 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003110 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3111 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
3112 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
3113 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003114
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003115 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003116 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003117
agable@chromium.org42c20792013-09-12 17:34:49 +00003118 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003119 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003120 return '\n'.join(self._description_lines)
3121
3122 def set_description(self, desc):
3123 if isinstance(desc, basestring):
3124 lines = desc.splitlines()
3125 else:
3126 lines = [line.rstrip() for line in desc]
3127 while lines and not lines[0]:
3128 lines.pop(0)
3129 while lines and not lines[-1]:
3130 lines.pop(-1)
3131 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003132
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003133 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3134 """Rewrites the R=/TBR= line(s) as a single line each.
3135
3136 Args:
3137 reviewers (list(str)) - list of additional emails to use for reviewers.
3138 tbrs (list(str)) - list of additional emails to use for TBRs.
3139 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3140 the change that are missing OWNER coverage. If this is not None, you
3141 must also pass a value for `change`.
3142 change (Change) - The Change that should be used for OWNERS lookups.
3143 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003144 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003145 assert isinstance(tbrs, list), tbrs
3146
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003147 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003148 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003149
3150 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003151 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003152
3153 reviewers = set(reviewers)
3154 tbrs = set(tbrs)
3155 LOOKUP = {
3156 'TBR': tbrs,
3157 'R': reviewers,
3158 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003159
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003160 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003161 regexp = re.compile(self.R_LINE)
3162 matches = [regexp.match(line) for line in self._description_lines]
3163 new_desc = [l for i, l in enumerate(self._description_lines)
3164 if not matches[i]]
3165 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003166
agable@chromium.org42c20792013-09-12 17:34:49 +00003167 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003168
3169 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003170 for match in matches:
3171 if not match:
3172 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003173 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3174
3175 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003176 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003177 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003178 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003179 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003180 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003181 LOOKUP[add_owners_to].update(
3182 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003183
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003184 # If any folks ended up in both groups, remove them from tbrs.
3185 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003186
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003187 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3188 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003189
3190 # Put the new lines in the description where the old first R= line was.
3191 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3192 if 0 <= line_loc < len(self._description_lines):
3193 if new_tbr_line:
3194 self._description_lines.insert(line_loc, new_tbr_line)
3195 if new_r_line:
3196 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003197 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003198 if new_r_line:
3199 self.append_footer(new_r_line)
3200 if new_tbr_line:
3201 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003202
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00003203 def set_preserve_tryjobs(self):
3204 """Ensures description footer contains 'Cq-Do-Not-Cancel-Tryjobs: true'."""
3205 footers = git_footers.parse_footers(self.description)
3206 for v in footers.get('Cq-Do-Not-Cancel-Tryjobs', []):
3207 if v.lower() == 'true':
3208 return
3209 self.append_footer('Cq-Do-Not-Cancel-Tryjobs: true')
3210
Aaron Gable3a16ed12017-03-23 10:51:55 -07003211 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003212 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003213 self.set_description([
3214 '# Enter a description of the change.',
3215 '# This will be displayed on the codereview site.',
3216 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003217 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003218 '--------------------',
3219 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003220
agable@chromium.org42c20792013-09-12 17:34:49 +00003221 regexp = re.compile(self.BUG_LINE)
Jonas Termansend0f79112019-03-22 15:28:26 +00003222 prefix = settings.GetBugPrefix()
agable@chromium.org42c20792013-09-12 17:34:49 +00003223 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003224 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003225 if git_footer:
3226 self.append_footer('Bug: %s' % ', '.join(values))
3227 else:
3228 for value in values:
3229 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003230
agable@chromium.org42c20792013-09-12 17:34:49 +00003231 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003232 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003233 if not content:
3234 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003235 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003236
Bruce Dawson2377b012018-01-11 16:46:49 -08003237 # Strip off comments and default inserted "Bug:" line.
3238 clean_lines = [line.rstrip() for line in lines if not
Jonas Termansend0f79112019-03-22 15:28:26 +00003239 (line.startswith('#') or
3240 line.rstrip() == "Bug:" or
3241 line.rstrip() == "Bug: " + prefix)]
agable@chromium.org42c20792013-09-12 17:34:49 +00003242 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003243 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003244 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003245
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003246 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003247 """Adds a footer line to the description.
3248
3249 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3250 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3251 that Gerrit footers are always at the end.
3252 """
3253 parsed_footer_line = git_footers.parse_footer(line)
3254 if parsed_footer_line:
3255 # Line is a gerrit footer in the form: Footer-Key: any value.
3256 # Thus, must be appended observing Gerrit footer rules.
3257 self.set_description(
3258 git_footers.add_footer(self.description,
3259 key=parsed_footer_line[0],
3260 value=parsed_footer_line[1]))
3261 return
3262
3263 if not self._description_lines:
3264 self._description_lines.append(line)
3265 return
3266
3267 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3268 if gerrit_footers:
3269 # git_footers.split_footers ensures that there is an empty line before
3270 # actual (gerrit) footers, if any. We have to keep it that way.
3271 assert top_lines and top_lines[-1] == ''
3272 top_lines, separator = top_lines[:-1], top_lines[-1:]
3273 else:
3274 separator = [] # No need for separator if there are no gerrit_footers.
3275
3276 prev_line = top_lines[-1] if top_lines else ''
3277 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3278 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3279 top_lines.append('')
3280 top_lines.append(line)
3281 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003282
tandrii99a72f22016-08-17 14:33:24 -07003283 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003284 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003285 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003286 reviewers = [match.group(2).strip()
3287 for match in matches
3288 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003289 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003290
bradnelsond975b302016-10-23 12:20:23 -07003291 def get_cced(self):
3292 """Retrieves the list of reviewers."""
3293 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3294 cced = [match.group(2).strip() for match in matches if match]
3295 return cleanup_list(cced)
3296
Nodir Turakulov23b82142017-11-16 11:04:25 -08003297 def get_hash_tags(self):
3298 """Extracts and sanitizes a list of Gerrit hashtags."""
3299 subject = (self._description_lines or ('',))[0]
3300 subject = re.sub(
3301 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3302
3303 tags = []
3304 start = 0
3305 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3306 while True:
3307 m = bracket_exp.match(subject, start)
3308 if not m:
3309 break
3310 tags.append(self.sanitize_hash_tag(m.group(1)))
3311 start = m.end()
3312
3313 if not tags:
3314 # Try "Tag: " prefix.
3315 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3316 if m:
3317 tags.append(self.sanitize_hash_tag(m.group(1)))
3318 return tags
3319
3320 @classmethod
3321 def sanitize_hash_tag(cls, tag):
3322 """Returns a sanitized Gerrit hash tag.
3323
3324 A sanitized hashtag can be used as a git push refspec parameter value.
3325 """
3326 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3327
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003328 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3329 """Updates this commit description given the parent.
3330
3331 This is essentially what Gnumbd used to do.
3332 Consult https://goo.gl/WMmpDe for more details.
3333 """
3334 assert parent_msg # No, orphan branch creation isn't supported.
3335 assert parent_hash
3336 assert dest_ref
3337 parent_footer_map = git_footers.parse_footers(parent_msg)
3338 # This will also happily parse svn-position, which GnumbD is no longer
3339 # supporting. While we'd generate correct footers, the verifier plugin
3340 # installed in Gerrit will block such commit (ie git push below will fail).
3341 parent_position = git_footers.get_position(parent_footer_map)
3342
3343 # Cherry-picks may have last line obscuring their prior footers,
3344 # from git_footers perspective. This is also what Gnumbd did.
3345 cp_line = None
3346 if (self._description_lines and
3347 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3348 cp_line = self._description_lines.pop()
3349
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003350 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003351
3352 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3353 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003354 for i, line in enumerate(footer_lines):
3355 k, v = git_footers.parse_footer(line) or (None, None)
3356 if k and k.startswith('Cr-'):
3357 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003358
3359 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003360 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003361 if parent_position[0] == dest_ref:
3362 # Same branch as parent.
3363 number = int(parent_position[1]) + 1
3364 else:
3365 number = 1 # New branch, and extra lineage.
3366 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3367 int(parent_position[1])))
3368
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003369 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3370 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003371
3372 self._description_lines = top_lines
3373 if cp_line:
3374 self._description_lines.append(cp_line)
3375 if self._description_lines[-1] != '':
3376 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003377 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003378
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003379
Aaron Gablea1bab272017-04-11 16:38:18 -07003380def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003381 """Retrieves the reviewers that approved a CL from the issue properties with
3382 messages.
3383
3384 Note that the list may contain reviewers that are not committer, thus are not
3385 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003386
3387 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003388 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003389 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003390 return sorted(
3391 set(
3392 message['sender']
3393 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003394 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003395 )
3396 )
3397
3398
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003399def FindCodereviewSettingsFile(filename='codereview.settings'):
3400 """Finds the given file starting in the cwd and going up.
3401
3402 Only looks up to the top of the repository unless an
3403 'inherit-review-settings-ok' file exists in the root of the repository.
3404 """
3405 inherit_ok_file = 'inherit-review-settings-ok'
3406 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003407 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003408 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3409 root = '/'
3410 while True:
3411 if filename in os.listdir(cwd):
3412 if os.path.isfile(os.path.join(cwd, filename)):
3413 return open(os.path.join(cwd, filename))
3414 if cwd == root:
3415 break
3416 cwd = os.path.dirname(cwd)
3417
3418
3419def LoadCodereviewSettingsFromFile(fileobj):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003420 """Parses a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003421 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003422
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003423 def SetProperty(name, setting, unset_error_ok=False):
3424 fullname = 'rietveld.' + name
3425 if setting in keyvals:
3426 RunGit(['config', fullname, keyvals[setting]])
3427 else:
3428 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3429
tandrii48df5812016-10-17 03:55:37 -07003430 if not keyvals.get('GERRIT_HOST', False):
3431 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003432 # Only server setting is required. Other settings can be absent.
3433 # In that case, we ignore errors raised during option deletion attempt.
3434 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
3435 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3436 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003437 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003438 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3439 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003440 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3441 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003442
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003443 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003444 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003445
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003446 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003447 RunGit(['config', 'gerrit.squash-uploads',
3448 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003449
tandrii@chromium.org28253532016-04-14 13:46:56 +00003450 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003451 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003452 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3453
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003454 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003455 # should be of the form
3456 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3457 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003458 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3459 keyvals['ORIGIN_URL_CONFIG']])
3460
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003461
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003462def urlretrieve(source, destination):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003463 """Downloads a network object to a local file, like urllib.urlretrieve.
3464
3465 This is necessary because urllib is broken for SSL connections via a proxy.
3466 """
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003467 with open(destination, 'w') as f:
3468 f.write(urllib2.urlopen(source).read())
3469
3470
ukai@chromium.org712d6102013-11-27 00:52:58 +00003471def hasSheBang(fname):
3472 """Checks fname is a #! script."""
3473 with open(fname) as f:
3474 return f.read(2).startswith('#!')
3475
3476
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003477# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3478def DownloadHooks(*args, **kwargs):
3479 pass
3480
3481
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003482def DownloadGerritHook(force):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003483 """Downloads and installs a Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003484
3485 Args:
3486 force: True to update hooks. False to install hooks if not present.
3487 """
3488 if not settings.GetIsGerrit():
3489 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003490 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003491 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3492 if not os.access(dst, os.X_OK):
3493 if os.path.exists(dst):
3494 if not force:
3495 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003496 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003497 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003498 if not hasSheBang(dst):
3499 DieWithError('Not a script: %s\n'
3500 'You need to download from\n%s\n'
3501 'into .git/hooks/commit-msg and '
3502 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003503 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3504 except Exception:
3505 if os.path.exists(dst):
3506 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003507 DieWithError('\nFailed to download hooks.\n'
3508 'You need to download from\n%s\n'
3509 'into .git/hooks/commit-msg and '
3510 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003511
3512
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003513class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003514 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003515
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003516 _GOOGLESOURCE = 'googlesource.com'
3517
3518 def __init__(self):
3519 # Cached list of [host, identity, source], where source is either
3520 # .gitcookies or .netrc.
3521 self._all_hosts = None
3522
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003523 def ensure_configured_gitcookies(self):
3524 """Runs checks and suggests fixes to make git use .gitcookies from default
3525 path."""
3526 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3527 configured_path = RunGitSilent(
3528 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003529 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003530 if configured_path:
3531 self._ensure_default_gitcookies_path(configured_path, default)
3532 else:
3533 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003534
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003535 @staticmethod
3536 def _ensure_default_gitcookies_path(configured_path, default_path):
3537 assert configured_path
3538 if configured_path == default_path:
3539 print('git is already configured to use your .gitcookies from %s' %
3540 configured_path)
3541 return
3542
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003543 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003544 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3545 (configured_path, default_path))
3546
3547 if not os.path.exists(configured_path):
3548 print('However, your configured .gitcookies file is missing.')
3549 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3550 action='reconfigure')
3551 RunGit(['config', '--global', 'http.cookiefile', default_path])
3552 return
3553
3554 if os.path.exists(default_path):
3555 print('WARNING: default .gitcookies file already exists %s' %
3556 default_path)
3557 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3558 default_path)
3559
3560 confirm_or_exit('Move existing .gitcookies to default location?',
3561 action='move')
3562 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003563 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003564 print('Moved and reconfigured git to use .gitcookies from %s' %
3565 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003566
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003567 @staticmethod
3568 def _configure_gitcookies_path(default_path):
3569 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3570 if os.path.exists(netrc_path):
3571 print('You seem to be using outdated .netrc for git credentials: %s' %
3572 netrc_path)
3573 print('This tool will guide you through setting up recommended '
3574 '.gitcookies store for git credentials.\n'
3575 '\n'
3576 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3577 ' git config --global --unset http.cookiefile\n'
3578 ' mv %s %s.backup\n\n' % (default_path, default_path))
3579 confirm_or_exit(action='setup .gitcookies')
3580 RunGit(['config', '--global', 'http.cookiefile', default_path])
3581 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003582
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003583 def get_hosts_with_creds(self, include_netrc=False):
3584 if self._all_hosts is None:
3585 a = gerrit_util.CookiesAuthenticator()
3586 self._all_hosts = [
3587 (h, u, s)
3588 for h, u, s in itertools.chain(
3589 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3590 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3591 )
3592 if h.endswith(self._GOOGLESOURCE)
3593 ]
3594
3595 if include_netrc:
3596 return self._all_hosts
3597 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3598
3599 def print_current_creds(self, include_netrc=False):
3600 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3601 if not hosts:
3602 print('No Git/Gerrit credentials found')
3603 return
3604 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3605 header = [('Host', 'User', 'Which file'),
3606 ['=' * l for l in lengths]]
3607 for row in (header + hosts):
3608 print('\t'.join((('%%+%ds' % l) % s)
3609 for l, s in zip(lengths, row)))
3610
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003611 @staticmethod
3612 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003613 """Parses identity "git-<username>.domain" into <username> and domain."""
3614 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003615 # distinguishable from sub-domains. But we do know typical domains:
3616 if identity.endswith('.chromium.org'):
3617 domain = 'chromium.org'
3618 username = identity[:-len('.chromium.org')]
3619 else:
3620 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003621 if username.startswith('git-'):
3622 username = username[len('git-'):]
3623 return username, domain
3624
3625 def _get_usernames_of_domain(self, domain):
3626 """Returns list of usernames referenced by .gitcookies in a given domain."""
3627 identities_by_domain = {}
3628 for _, identity, _ in self.get_hosts_with_creds():
3629 username, domain = self._parse_identity(identity)
3630 identities_by_domain.setdefault(domain, []).append(username)
3631 return identities_by_domain.get(domain)
3632
3633 def _canonical_git_googlesource_host(self, host):
3634 """Normalizes Gerrit hosts (with '-review') to Git host."""
3635 assert host.endswith(self._GOOGLESOURCE)
3636 # Prefix doesn't include '.' at the end.
3637 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3638 if prefix.endswith('-review'):
3639 prefix = prefix[:-len('-review')]
3640 return prefix + '.' + self._GOOGLESOURCE
3641
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003642 def _canonical_gerrit_googlesource_host(self, host):
3643 git_host = self._canonical_git_googlesource_host(host)
3644 prefix = git_host.split('.', 1)[0]
3645 return prefix + '-review.' + self._GOOGLESOURCE
3646
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003647 def _get_counterpart_host(self, host):
3648 assert host.endswith(self._GOOGLESOURCE)
3649 git = self._canonical_git_googlesource_host(host)
3650 gerrit = self._canonical_gerrit_googlesource_host(git)
3651 return git if gerrit == host else gerrit
3652
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003653 def has_generic_host(self):
3654 """Returns whether generic .googlesource.com has been configured.
3655
3656 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3657 """
3658 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3659 if host == '.' + self._GOOGLESOURCE:
3660 return True
3661 return False
3662
3663 def _get_git_gerrit_identity_pairs(self):
3664 """Returns map from canonic host to pair of identities (Git, Gerrit).
3665
3666 One of identities might be None, meaning not configured.
3667 """
3668 host_to_identity_pairs = {}
3669 for host, identity, _ in self.get_hosts_with_creds():
3670 canonical = self._canonical_git_googlesource_host(host)
3671 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3672 idx = 0 if canonical == host else 1
3673 pair[idx] = identity
3674 return host_to_identity_pairs
3675
3676 def get_partially_configured_hosts(self):
3677 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003678 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3679 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3680 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003681
3682 def get_conflicting_hosts(self):
3683 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003684 host
3685 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003686 if None not in (i1, i2) and i1 != i2)
3687
3688 def get_duplicated_hosts(self):
3689 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3690 return set(host for host, count in counters.iteritems() if count > 1)
3691
3692 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3693 'chromium.googlesource.com': 'chromium.org',
3694 'chrome-internal.googlesource.com': 'google.com',
3695 }
3696
3697 def get_hosts_with_wrong_identities(self):
3698 """Finds hosts which **likely** reference wrong identities.
3699
3700 Note: skips hosts which have conflicting identities for Git and Gerrit.
3701 """
3702 hosts = set()
3703 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3704 pair = self._get_git_gerrit_identity_pairs().get(host)
3705 if pair and pair[0] == pair[1]:
3706 _, domain = self._parse_identity(pair[0])
3707 if domain != expected:
3708 hosts.add(host)
3709 return hosts
3710
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003711 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003712 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003713 hosts = sorted(hosts)
3714 assert hosts
3715 if extra_column_func is None:
3716 extras = [''] * len(hosts)
3717 else:
3718 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003719 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3720 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003721 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003722 lines.append(tmpl % he)
3723 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003724
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003725 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003726 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003727 yield ('.googlesource.com wildcard record detected',
3728 ['Chrome Infrastructure team recommends to list full host names '
3729 'explicitly.'],
3730 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003731
3732 dups = self.get_duplicated_hosts()
3733 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003734 yield ('The following hosts were defined twice',
3735 self._format_hosts(dups),
3736 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003737
3738 partial = self.get_partially_configured_hosts()
3739 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003740 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3741 'These hosts are missing',
3742 self._format_hosts(partial, lambda host: 'but %s defined' %
3743 self._get_counterpart_host(host)),
3744 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003745
3746 conflicting = self.get_conflicting_hosts()
3747 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003748 yield ('The following Git hosts have differing credentials from their '
3749 'Gerrit counterparts',
3750 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3751 tuple(self._get_git_gerrit_identity_pairs()[host])),
3752 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003753
3754 wrong = self.get_hosts_with_wrong_identities()
3755 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003756 yield ('These hosts likely use wrong identity',
3757 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
3758 (self._get_git_gerrit_identity_pairs()[host][0],
3759 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
3760 wrong)
3761
3762 def find_and_report_problems(self):
3763 """Returns True if there was at least one problem, else False."""
3764 found = False
3765 bad_hosts = set()
3766 for title, sublines, hosts in self._find_problems():
3767 if not found:
3768 found = True
3769 print('\n\n.gitcookies problem report:\n')
3770 bad_hosts.update(hosts or [])
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003771 print(' %s%s' % (title, (':' if sublines else '')))
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003772 if sublines:
3773 print()
3774 print(' %s' % '\n '.join(sublines))
3775 print()
3776
3777 if bad_hosts:
3778 assert found
3779 print(' You can manually remove corresponding lines in your %s file and '
3780 'visit the following URLs with correct account to generate '
3781 'correct credential lines:\n' %
3782 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
3783 print(' %s' % '\n '.join(sorted(set(
3784 gerrit_util.CookiesAuthenticator().get_new_password_url(
3785 self._canonical_git_googlesource_host(host))
3786 for host in bad_hosts
3787 ))))
3788 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003789
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003790
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003791@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003792def CMDcreds_check(parser, args):
3793 """Checks credentials and suggests changes."""
3794 _, _ = parser.parse_args(args)
3795
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003796 # Code below checks .gitcookies. Abort if using something else.
3797 authn = gerrit_util.Authenticator.get()
3798 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
3799 if isinstance(authn, gerrit_util.GceAuthenticator):
3800 DieWithError(
3801 'This command is not designed for GCE, are you on a bot?\n'
3802 'If you need to run this on GCE, export SKIP_GCE_AUTH_FOR_GIT=1 '
3803 'in your env.')
Aaron Gabled10ca0e2017-09-11 11:24:10 -07003804 DieWithError(
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003805 'This command is not designed for bot environment. It checks '
3806 '~/.gitcookies file not generally used on bots.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003807
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003808 checker = _GitCookiesChecker()
3809 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003810
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003811 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003812 checker.print_current_creds(include_netrc=True)
3813
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003814 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003815 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003816 return 0
3817 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003818
3819
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003820@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003821def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003822 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003823 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3824 branch = ShortBranchName(branchref)
3825 _, args = parser.parse_args(args)
3826 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003827 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003828 return RunGit(['config', 'branch.%s.base-url' % branch],
3829 error_ok=False).strip()
3830 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003831 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003832 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3833 error_ok=False).strip()
3834
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003835
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003836def color_for_status(status):
3837 """Maps a Changelist status to color, for CMDstatus and other tools."""
3838 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07003839 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003840 'waiting': Fore.BLUE,
3841 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07003842 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003843 'lgtm': Fore.GREEN,
3844 'commit': Fore.MAGENTA,
3845 'closed': Fore.CYAN,
3846 'error': Fore.WHITE,
3847 }.get(status, Fore.WHITE)
3848
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003849
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003850def get_cl_statuses(changes, fine_grained, max_processes=None):
3851 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003852
3853 If fine_grained is true, this will fetch CL statuses from the server.
3854 Otherwise, simply indicate if there's a matching url for the given branches.
3855
3856 If max_processes is specified, it is used as the maximum number of processes
3857 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3858 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003859
3860 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003861 """
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003862 if not changes:
3863 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003864
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003865 if not fine_grained:
3866 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07003867 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003868 for cl in changes:
3869 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003870 return
3871
3872 # First, sort out authentication issues.
3873 logging.debug('ensuring credentials exist')
3874 for cl in changes:
3875 cl.EnsureAuthenticated(force=False, refresh=True)
3876
3877 def fetch(cl):
3878 try:
3879 return (cl, cl.GetStatus())
3880 except:
3881 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07003882 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003883 raise
3884
3885 threads_count = len(changes)
3886 if max_processes:
3887 threads_count = max(1, min(threads_count, max_processes))
3888 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3889
3890 pool = ThreadPool(threads_count)
3891 fetched_cls = set()
3892 try:
3893 it = pool.imap_unordered(fetch, changes).__iter__()
3894 while True:
3895 try:
3896 cl, status = it.next(timeout=5)
3897 except multiprocessing.TimeoutError:
3898 break
3899 fetched_cls.add(cl)
3900 yield cl, status
3901 finally:
3902 pool.close()
3903
3904 # Add any branches that failed to fetch.
3905 for cl in set(changes) - fetched_cls:
3906 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003907
rmistry@google.com2dd99862015-06-22 12:22:18 +00003908
3909def upload_branch_deps(cl, args):
3910 """Uploads CLs of local branches that are dependents of the current branch.
3911
3912 If the local branch dependency tree looks like:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003913
3914 test1 -> test2.1 -> test3.1
3915 -> test3.2
3916 -> test2.2 -> test3.3
rmistry@google.com2dd99862015-06-22 12:22:18 +00003917
3918 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3919 run on the dependent branches in this order:
3920 test2.1, test3.1, test3.2, test2.2, test3.3
3921
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003922 Note: This function does not rebase your local dependent branches. Use it
3923 when you make a change to the parent branch that will not conflict
3924 with its dependent branches, and you would like their dependencies
3925 updated in Rietveld.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003926 """
3927 if git_common.is_dirty_git_tree('upload-branch-deps'):
3928 return 1
3929
3930 root_branch = cl.GetBranch()
3931 if root_branch is None:
3932 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3933 'Get on a branch!')
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00003934 if not cl.GetIssue():
rmistry@google.com2dd99862015-06-22 12:22:18 +00003935 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3936 'patchset dependencies without an uploaded CL.')
3937
3938 branches = RunGit(['for-each-ref',
3939 '--format=%(refname:short) %(upstream:short)',
3940 'refs/heads'])
3941 if not branches:
3942 print('No local branches found.')
3943 return 0
3944
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003945 # Create a dictionary of all local branches to the branches that are
3946 # dependent on it.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003947 tracked_to_dependents = collections.defaultdict(list)
3948 for b in branches.splitlines():
3949 tokens = b.split()
3950 if len(tokens) == 2:
3951 branch_name, tracked = tokens
3952 tracked_to_dependents[tracked].append(branch_name)
3953
vapiera7fbd5a2016-06-16 09:17:49 -07003954 print()
3955 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003956 dependents = []
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003957
rmistry@google.com2dd99862015-06-22 12:22:18 +00003958 def traverse_dependents_preorder(branch, padding=''):
3959 dependents_to_process = tracked_to_dependents.get(branch, [])
3960 padding += ' '
3961 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003962 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003963 dependents.append(dependent)
3964 traverse_dependents_preorder(dependent, padding)
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003965
rmistry@google.com2dd99862015-06-22 12:22:18 +00003966 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003967 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003968
3969 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003970 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003971 return 0
3972
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003973 confirm_or_exit('This command will checkout all dependent branches and run '
3974 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003975
rmistry@google.com2dd99862015-06-22 12:22:18 +00003976 # Record all dependents that failed to upload.
3977 failures = {}
3978 # Go through all dependents, checkout the branch and upload.
3979 try:
3980 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003981 print()
3982 print('--------------------------------------')
3983 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003984 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003985 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003986 try:
3987 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003988 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003989 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003990 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003991 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003992 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003993 finally:
3994 # Swap back to the original root branch.
3995 RunGit(['checkout', '-q', root_branch])
3996
vapiera7fbd5a2016-06-16 09:17:49 -07003997 print()
3998 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003999 for dependent_branch in dependents:
4000 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07004001 print(' %s : %s' % (dependent_branch, upload_status))
4002 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004003
4004 return 0
4005
4006
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004007@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07004008def CMDarchive(parser, args):
4009 """Archives and deletes branches associated with closed changelists."""
4010 parser.add_option(
4011 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07004012 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07004013 parser.add_option(
4014 '-f', '--force', action='store_true',
4015 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07004016 parser.add_option(
4017 '-d', '--dry-run', action='store_true',
4018 help='Skip the branch tagging and removal steps.')
4019 parser.add_option(
4020 '-t', '--notags', action='store_true',
4021 help='Do not tag archived branches. '
4022 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004023
4024 auth.add_auth_options(parser)
4025 options, args = parser.parse_args(args)
4026 if args:
4027 parser.error('Unsupported args: %s' % ' '.join(args))
4028 auth_config = auth.extract_auth_config_from_options(options)
4029
4030 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4031 if not branches:
4032 return 0
4033
vapiera7fbd5a2016-06-16 09:17:49 -07004034 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004035 changes = [Changelist(branchref=b, auth_config=auth_config)
4036 for b in branches.splitlines()]
4037 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4038 statuses = get_cl_statuses(changes,
4039 fine_grained=True,
4040 max_processes=options.maxjobs)
4041 proposal = [(cl.GetBranch(),
4042 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4043 for cl, status in statuses
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00004044 if status in ('closed', 'rietveld-not-supported')]
kmarshall3bff56b2016-06-06 18:31:47 -07004045 proposal.sort()
4046
4047 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004048 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004049 return 0
4050
4051 current_branch = GetCurrentBranch()
4052
vapiera7fbd5a2016-06-16 09:17:49 -07004053 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004054 if options.notags:
4055 for next_item in proposal:
4056 print(' ' + next_item[0])
4057 else:
4058 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4059 for next_item in proposal:
4060 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004061
kmarshall9249e012016-08-23 12:02:16 -07004062 # Quit now on precondition failure or if instructed by the user, either
4063 # via an interactive prompt or by command line flags.
4064 if options.dry_run:
4065 print('\nNo changes were made (dry run).\n')
4066 return 0
4067 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004068 print('You are currently on a branch \'%s\' which is associated with a '
4069 'closed codereview issue, so archive cannot proceed. Please '
4070 'checkout another branch and run this command again.' %
4071 current_branch)
4072 return 1
kmarshall9249e012016-08-23 12:02:16 -07004073 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004074 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4075 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004076 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004077 return 1
4078
4079 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004080 if not options.notags:
4081 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004082 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004083
vapiera7fbd5a2016-06-16 09:17:49 -07004084 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004085
4086 return 0
4087
4088
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004089@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004090def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004091 """Show status of changelists.
4092
4093 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004094 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004095 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004096 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004097 - Red 'not LGTM'ed
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004098 - Magenta in the CQ
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004099 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004100 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004101
4102 Also see 'git cl comments'.
4103 """
Alan Cuttera3be9a52019-03-04 18:50:33 +00004104 parser.add_option(
4105 '--no-branch-color',
4106 action='store_true',
4107 help='Disable colorized branch names')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004108 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004109 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004110 parser.add_option('-f', '--fast', action='store_true',
4111 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004112 parser.add_option(
4113 '-j', '--maxjobs', action='store', type=int,
4114 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004115
4116 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004117 _add_codereview_issue_select_options(
4118 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004119 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004120 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004121 if args:
4122 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004123 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004124
iannuccie53c9352016-08-17 14:40:40 -07004125 if options.issue is not None and not options.field:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004126 parser.error('--field must be specified with --issue.')
iannucci3c972b92016-08-17 13:24:10 -07004127
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004128 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004129 cl = Changelist(auth_config=auth_config, issue=options.issue,
4130 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004131 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004132 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004133 elif options.field == 'id':
4134 issueid = cl.GetIssue()
4135 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004136 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004137 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004138 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004139 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004140 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004141 elif options.field == 'status':
4142 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004143 elif options.field == 'url':
4144 url = cl.GetIssueURL()
4145 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004146 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004147 return 0
4148
4149 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4150 if not branches:
4151 print('No local branch found.')
4152 return 0
4153
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004154 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004155 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004156 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004157 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004158 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004159 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004160 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004161
Daniel McArdlea23bf592019-02-12 00:25:12 +00004162 current_branch = GetCurrentBranch()
4163
4164 def FormatBranchName(branch, colorize=False):
4165 """Simulates 'git branch' behavior. Colorizes and prefixes branch name with
4166 an asterisk when it is the current branch."""
4167
4168 asterisk = ""
4169 color = Fore.RESET
4170 if branch == current_branch:
4171 asterisk = "* "
4172 color = Fore.GREEN
4173 branch_name = ShortBranchName(branch)
4174
4175 if colorize:
4176 return asterisk + color + branch_name + Fore.RESET
Daniel McArdle452a49f2019-02-14 17:28:31 +00004177 return asterisk + branch_name
4178
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004179 branch_statuses = {}
Daniel McArdlea23bf592019-02-12 00:25:12 +00004180
4181 alignment = max(5, max(len(FormatBranchName(c.GetBranch())) for c in changes))
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004182 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4183 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004184 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004185 c, status = output.next()
4186 branch_statuses[c.GetBranch()] = status
4187 status = branch_statuses.pop(branch)
4188 url = cl.GetIssueURL()
4189 if url and (not status or status == 'error'):
4190 # The issue probably doesn't exist anymore.
4191 url += ' (broken)'
4192
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004193 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004194 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004195 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004196 color = ''
4197 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004198 status_str = '(%s)' % status if status else ''
Daniel McArdle452a49f2019-02-14 17:28:31 +00004199
Alan Cuttera3be9a52019-03-04 18:50:33 +00004200 branch_display = FormatBranchName(branch)
4201 padding = ' ' * (alignment - len(branch_display))
4202 if not options.no_branch_color:
4203 branch_display = FormatBranchName(branch, colorize=True)
Daniel McArdle452a49f2019-02-14 17:28:31 +00004204
Alan Cuttera3be9a52019-03-04 18:50:33 +00004205 print(' %s : %s%s %s%s' % (padding + branch_display, color, url,
4206 status_str, reset))
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004207
vapiera7fbd5a2016-06-16 09:17:49 -07004208 print()
Daniel McArdlea23bf592019-02-12 00:25:12 +00004209 print('Current branch: %s' % current_branch)
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004210 for cl in changes:
Daniel McArdlea23bf592019-02-12 00:25:12 +00004211 if cl.GetBranch() == current_branch:
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004212 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004213 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004214 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004215 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004216 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004217 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004218 print('Issue description:')
4219 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004220 return 0
4221
4222
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004223def colorize_CMDstatus_doc():
4224 """To be called once in main() to add colors to git cl status help."""
4225 colors = [i for i in dir(Fore) if i[0].isupper()]
4226
4227 def colorize_line(line):
4228 for color in colors:
4229 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004230 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004231 indent = len(line) - len(line.lstrip(' ')) + 1
4232 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4233 return line
4234
4235 lines = CMDstatus.__doc__.splitlines()
4236 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4237
4238
phajdan.jre328cf92016-08-22 04:12:17 -07004239def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004240 if path == '-':
4241 json.dump(contents, sys.stdout)
4242 else:
4243 with open(path, 'w') as f:
4244 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004245
4246
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004247@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004248@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004249def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004250 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004251
4252 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004253 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004254 parser.add_option('-r', '--reverse', action='store_true',
4255 help='Lookup the branch(es) for the specified issues. If '
4256 'no issues are specified, all branches with mapped '
4257 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004258 parser.add_option('--json',
4259 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004260 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004261 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004262 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004263
dnj@chromium.org406c4402015-03-03 17:22:28 +00004264 if options.reverse:
4265 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004266 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004267 # Reverse issue lookup.
4268 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004269
4270 git_config = {}
4271 for config in RunGit(['config', '--get-regexp',
4272 r'branch\..*issue']).splitlines():
4273 name, _space, val = config.partition(' ')
4274 git_config[name] = val
4275
dnj@chromium.org406c4402015-03-03 17:22:28 +00004276 for branch in branches:
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004277 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
4278 config_key = _git_branch_config_key(ShortBranchName(branch),
4279 cls.IssueConfigKey())
4280 issue = git_config.get(config_key)
4281 if issue:
4282 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004283 if not args:
4284 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004285 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004286 for issue in args:
Lei Zhang5a368d42019-03-25 23:18:19 +00004287 try:
4288 issue_num = int(issue)
4289 except ValueError:
4290 print('ERROR cannot parse issue number: %s' % issue, file=sys.stderr)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004291 continue
Lei Zhang5a368d42019-03-25 23:18:19 +00004292 result[issue_num] = issue_branch_map.get(issue_num)
vapiera7fbd5a2016-06-16 09:17:49 -07004293 print('Branch for issue number %s: %s' % (
Lei Zhang5a368d42019-03-25 23:18:19 +00004294 issue, ', '.join(issue_branch_map.get(issue_num) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004295 if options.json:
4296 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004297 return 0
4298
4299 if len(args) > 0:
4300 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4301 if not issue.valid:
4302 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4303 'or no argument to list it.\n'
4304 'Maybe you want to run git cl status?')
4305 cl = Changelist(codereview=issue.codereview)
4306 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004307 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004308 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004309 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4310 if options.json:
4311 write_json(options.json, {
4312 'issue': cl.GetIssue(),
4313 'issue_url': cl.GetIssueURL(),
4314 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004315 return 0
4316
4317
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004318@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004319def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004320 """Shows or posts review comments for any changelist."""
4321 parser.add_option('-a', '--add-comment', dest='comment',
4322 help='comment to add to an issue')
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004323 parser.add_option('-p', '--publish', action='store_true',
4324 help='marks CL as ready and sends comment to reviewers')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004325 parser.add_option('-i', '--issue', dest='issue',
4326 help='review issue id (defaults to current issue). '
4327 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004328 parser.add_option('-m', '--machine-readable', dest='readable',
4329 action='store_false', default=True,
4330 help='output comments in a format compatible with '
4331 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004332 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004333 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004334 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004335 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004336 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004337 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004338 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004339
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004340 issue = None
4341 if options.issue:
4342 try:
4343 issue = int(options.issue)
4344 except ValueError:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004345 DieWithError('A review issue ID is expected to be a number.')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004346
Andrii Shyshkalov642641d2018-10-16 05:54:41 +00004347 cl = Changelist(issue=issue, codereview='gerrit', auth_config=auth_config)
4348
4349 if not cl.IsGerrit():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004350 parser.error('Rietveld is not supported.')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004351
4352 if options.comment:
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004353 cl.AddComment(options.comment, options.publish)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004354 return 0
4355
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004356 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4357 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004358 for comment in summary:
4359 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004360 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004361 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004362 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004363 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004364 color = Fore.MAGENTA
Quinten Yearsley0e617c02019-02-20 00:37:03 +00004365 elif comment.autogenerated:
4366 color = Fore.CYAN
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004367 else:
4368 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004369 print('\n%s%s %s%s\n%s' % (
4370 color,
4371 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4372 comment.sender,
4373 Fore.RESET,
4374 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4375
smut@google.comc85ac942015-09-15 16:34:43 +00004376 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004377 def pre_serialize(c):
4378 dct = c.__dict__.copy()
4379 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4380 return dct
Leszek Swirski45b20c42018-09-17 17:05:26 +00004381 write_json(options.json_file, map(pre_serialize, summary))
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004382 return 0
4383
4384
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004385@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004386@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004387def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004388 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004389 parser.add_option('-d', '--display', action='store_true',
4390 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004391 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004392 help='New description to set for this issue (- for stdin, '
4393 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004394 parser.add_option('-f', '--force', action='store_true',
4395 help='Delete any unpublished Gerrit edits for this issue '
4396 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004397
4398 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004399 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004400 options, args = parser.parse_args(args)
4401 _process_codereview_select_options(parser, options)
4402
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004403 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004404 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004405 target_issue_arg = ParseIssueNumberArgument(args[0],
4406 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004407 if not target_issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004408 parser.error('Invalid issue ID or URL.')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004409
martiniss6eda05f2016-06-30 10:18:35 -07004410 kwargs = {
Andrii Shyshkalovdd672fb2018-10-16 06:09:51 +00004411 'auth_config': auth.extract_auth_config_from_options(options),
4412 'codereview': options.forced_codereview,
martiniss6eda05f2016-06-30 10:18:35 -07004413 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004414 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004415 if target_issue_arg:
4416 kwargs['issue'] = target_issue_arg.issue
4417 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004418 if target_issue_arg.codereview and not options.forced_codereview:
4419 detected_codereview_from_url = True
4420 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004421
4422 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004423 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004424 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004425 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004426
4427 if detected_codereview_from_url:
4428 logging.info('canonical issue/change URL: %s (type: %s)\n',
4429 cl.GetIssueURL(), target_issue_arg.codereview)
4430
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004431 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004432
smut@google.com34fb6b12015-07-13 20:03:26 +00004433 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004434 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004435 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004436
4437 if options.new_description:
4438 text = options.new_description
4439 if text == '-':
4440 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004441 elif text == '+':
4442 base_branch = cl.GetCommonAncestorWithUpstream()
4443 change = cl.GetChange(base_branch, None, local_description=True)
4444 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004445
4446 description.set_description(text)
4447 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004448 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004449
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004450 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004451 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004452 return 0
4453
4454
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004455@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004456def CMDlint(parser, args):
4457 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004458 parser.add_option('--filter', action='append', metavar='-x,+y',
4459 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004460 auth.add_auth_options(parser)
4461 options, args = parser.parse_args(args)
4462 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004463
4464 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004465 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004466 try:
4467 import cpplint
4468 import cpplint_chromium
4469 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004470 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004471 return 1
4472
4473 # Change the current working directory before calling lint so that it
4474 # shows the correct base.
4475 previous_cwd = os.getcwd()
4476 os.chdir(settings.GetRoot())
4477 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004478 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004479 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4480 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004481 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004482 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004483 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004484
4485 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004486 command = args + files
4487 if options.filter:
4488 command = ['--filter=' + ','.join(options.filter)] + command
4489 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004490
4491 white_regex = re.compile(settings.GetLintRegex())
4492 black_regex = re.compile(settings.GetLintIgnoreRegex())
4493 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4494 for filename in filenames:
4495 if white_regex.match(filename):
4496 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004497 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004498 else:
4499 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4500 extra_check_functions)
4501 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004502 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004503 finally:
4504 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004505 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004506 if cpplint._cpplint_state.error_count != 0:
4507 return 1
4508 return 0
4509
4510
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004511@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004512def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004513 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004514 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004515 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004516 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004517 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004518 parser.add_option('--all', action='store_true',
4519 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004520 parser.add_option('--parallel', action='store_true',
4521 help='Run all tests specified by input_api.RunTests in all '
4522 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004523 auth.add_auth_options(parser)
4524 options, args = parser.parse_args(args)
4525 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004526
sbc@chromium.org71437c02015-04-09 19:29:40 +00004527 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004528 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004529 return 1
4530
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004531 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004532 if args:
4533 base_branch = args[0]
4534 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004535 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004536 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004537
Aaron Gable8076c282017-11-29 14:39:41 -08004538 if options.all:
4539 base_change = cl.GetChange(base_branch, None)
4540 files = [('M', f) for f in base_change.AllFiles()]
4541 change = presubmit_support.GitChange(
4542 base_change.Name(),
4543 base_change.FullDescriptionText(),
4544 base_change.RepositoryRoot(),
4545 files,
4546 base_change.issue,
4547 base_change.patchset,
4548 base_change.author_email,
4549 base_change._upstream)
4550 else:
4551 change = cl.GetChange(base_branch, None)
4552
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004553 cl.RunHook(
4554 committing=not options.upload,
4555 may_prompt=False,
4556 verbose=options.verbose,
Edward Lesmes8e282792018-04-03 18:50:29 -04004557 change=change,
4558 parallel=options.parallel)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004559 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004560
4561
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004562def GenerateGerritChangeId(message):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004563 """Returns the Change ID footer value (Ixxxxxx...xxx).
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004564
4565 Works the same way as
4566 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4567 but can be called on demand on all platforms.
4568
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004569 The basic idea is to generate git hash of a state of the tree, original
4570 commit message, author/committer info and timestamps.
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004571 """
4572 lines = []
4573 tree_hash = RunGitSilent(['write-tree'])
4574 lines.append('tree %s' % tree_hash.strip())
4575 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4576 if code == 0:
4577 lines.append('parent %s' % parent.strip())
4578 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4579 lines.append('author %s' % author.strip())
4580 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4581 lines.append('committer %s' % committer.strip())
4582 lines.append('')
4583 # Note: Gerrit's commit-hook actually cleans message of some lines and
4584 # whitespace. This code is not doing this, but it clearly won't decrease
4585 # entropy.
4586 lines.append(message)
4587 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
Raul Tambreb946b232019-03-26 14:48:46 +00004588 stdin=('\n'.join(lines)).encode())
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004589 return 'I%s' % change_hash.strip()
4590
4591
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004592def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004593 """Computes the remote branch ref to use for the CL.
4594
4595 Args:
4596 remote (str): The git remote for the CL.
4597 remote_branch (str): The git remote branch for the CL.
4598 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004599 """
4600 if not (remote and remote_branch):
4601 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004602
wittman@chromium.org455dc922015-01-26 20:15:50 +00004603 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004604 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004605 # refs, which are then translated into the remote full symbolic refs
4606 # below.
4607 if '/' not in target_branch:
4608 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4609 else:
4610 prefix_replacements = (
4611 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4612 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4613 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4614 )
4615 match = None
4616 for regex, replacement in prefix_replacements:
4617 match = re.search(regex, target_branch)
4618 if match:
4619 remote_branch = target_branch.replace(match.group(0), replacement)
4620 break
4621 if not match:
4622 # This is a branch path but not one we recognize; use as-is.
4623 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004624 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4625 # Handle the refs that need to land in different refs.
4626 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004627
wittman@chromium.org455dc922015-01-26 20:15:50 +00004628 # Create the true path to the remote branch.
4629 # Does the following translation:
4630 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4631 # * refs/remotes/origin/master -> refs/heads/master
4632 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4633 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4634 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4635 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4636 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4637 'refs/heads/')
4638 elif remote_branch.startswith('refs/remotes/branch-heads'):
4639 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004640
wittman@chromium.org455dc922015-01-26 20:15:50 +00004641 return remote_branch
4642
4643
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004644def cleanup_list(l):
4645 """Fixes a list so that comma separated items are put as individual items.
4646
4647 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4648 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4649 """
4650 items = sum((i.split(',') for i in l), [])
4651 stripped_items = (i.strip() for i in items)
4652 return sorted(filter(None, stripped_items))
4653
4654
Aaron Gable4db38df2017-11-03 14:59:07 -07004655@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004656@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004657def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004658 """Uploads the current changelist to codereview.
4659
4660 Can skip dependency patchset uploads for a branch by running:
4661 git config branch.branch_name.skip-deps-uploads True
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004662 To unset, run:
rmistry@google.com78948ed2015-07-08 23:09:57 +00004663 git config --unset branch.branch_name.skip-deps-uploads
4664 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004665
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004666 If the name of the checked out branch starts with "bug-" or "fix-" followed
4667 by a bug number, this bug number is automatically populated in the CL
Dominic Battre7d1c4842017-10-27 09:17:28 +02004668 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004669
4670 If subject contains text in square brackets or has "<text>: " prefix, such
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004671 text(s) is treated as Gerrit hashtags. For example, CLs with subjects:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004672 [git-cl] add support for hashtags
4673 Foo bar: implement foo
4674 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004675 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004676 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4677 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004678 parser.add_option('--bypass-watchlists', action='store_true',
4679 dest='bypass_watchlists',
4680 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004681 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004682 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004683 parser.add_option('--message', '-m', dest='message',
4684 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004685 parser.add_option('-b', '--bug',
4686 help='pre-populate the bug number(s) for this issue. '
4687 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004688 parser.add_option('--message-file', dest='message_file',
4689 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004690 parser.add_option('--title', '-t', dest='title',
4691 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004692 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004693 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004694 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004695 parser.add_option('--tbrs',
4696 action='append', default=[],
4697 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004698 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004699 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004700 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004701 parser.add_option('--hashtag', dest='hashtags',
4702 action='append', default=[],
4703 help=('Gerrit hashtag for new CL; '
4704 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004705 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004706 help='send email to reviewer(s) and cc(s) immediately')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004707 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004708 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004709 metavar='TARGET',
4710 help='Apply CL to remote ref TARGET. ' +
4711 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004712 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004713 help='Squash multiple commits into one')
Mike Frysingera989d552019-08-14 20:51:23 +00004714 parser.add_option('--no-squash', action='store_false', dest='squash',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004715 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004716 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004717 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004718 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4719 const='TBR', help='add a set of OWNERS to TBR')
4720 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4721 const='R', help='add a set of OWNERS to R')
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004722 parser.add_option('-c', '--use-commit-queue', action='store_true',
4723 help='tell the CQ to commit this patchset; '
4724 'implies --send-mail')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004725 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4726 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004727 help='Send the patchset to do a CQ dry run right after '
4728 'upload.')
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00004729 parser.add_option('--preserve-tryjobs', action='store_true',
4730 help='instruct the CQ to let tryjobs running even after '
4731 'new patchsets are uploaded instead of canceling '
4732 'prior patchset\' tryjobs')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004733 parser.add_option('--dependencies', action='store_true',
4734 help='Uploads CLs of all the local branches that depend on '
4735 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04004736 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4737 help='Sends your change to the CQ after an approval. Only '
4738 'works on repos that have the Auto-Submit label '
4739 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04004740 parser.add_option('--parallel', action='store_true',
4741 help='Run all tests specified by input_api.RunTests in all '
4742 'PRESUBMIT files in parallel.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004743
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004744 parser.add_option('--no-autocc', action='store_true',
4745 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004746 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004747 help='Set the review private. This implies --no-autocc.')
4748
rmistry@google.com2dd99862015-06-22 12:22:18 +00004749 orig_args = args
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004750 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004751 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004752 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004753 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004754 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004755
sbc@chromium.org71437c02015-04-09 19:29:40 +00004756 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004757 return 1
4758
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004759 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004760 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004761 options.cc = cleanup_list(options.cc)
4762
tandriib80458a2016-06-23 12:20:07 -07004763 if options.message_file:
4764 if options.message:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004765 parser.error('Only one of --message and --message-file allowed.')
tandriib80458a2016-06-23 12:20:07 -07004766 options.message = gclient_utils.FileRead(options.message_file)
4767 options.message_file = None
4768
tandrii4d0545a2016-07-06 03:56:49 -07004769 if options.cq_dry_run and options.use_commit_queue:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004770 parser.error('Only one of --use-commit-queue and --cq-dry-run allowed.')
tandrii4d0545a2016-07-06 03:56:49 -07004771
Aaron Gableedbc4132017-09-11 13:22:28 -07004772 if options.use_commit_queue:
4773 options.send_mail = True
4774
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004775 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4776 settings.GetIsGerrit()
4777
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004778 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004779 if not cl.IsGerrit():
4780 # Error out with instructions for repos not yet configured for Gerrit.
4781 print('=====================================')
4782 print('NOTICE: Rietveld is no longer supported. '
4783 'You can upload changes to Gerrit with')
4784 print(' git cl upload --gerrit')
4785 print('or set Gerrit to be your default code review tool with')
4786 print(' git config gerrit.host true')
4787 print('=====================================')
4788 return 1
4789
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004790 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004791
4792
Francois Dorayd42c6812017-05-30 15:10:20 -04004793@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004794@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04004795def CMDsplit(parser, args):
4796 """Splits a branch into smaller branches and uploads CLs.
4797
4798 Creates a branch and uploads a CL for each group of files modified in the
4799 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004800 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04004801 the shared OWNERS file.
4802 """
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004803 parser.add_option('-d', '--description', dest='description_file',
4804 help='A text file containing a CL description in which '
4805 '$directory will be replaced by each CL\'s directory.')
4806 parser.add_option('-c', '--comment', dest='comment_file',
4807 help='A text file containing a CL comment.')
4808 parser.add_option('-n', '--dry-run', dest='dry_run', action='store_true',
Chris Watkinsba28e462017-12-13 11:22:17 +11004809 default=False,
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004810 help='List the files and reviewers for each CL that would '
4811 'be created, but don\'t create branches or CLs.')
4812 parser.add_option('--cq-dry-run', action='store_true',
4813 help='If set, will do a cq dry run for each uploaded CL. '
4814 'Please be careful when doing this; more than ~10 CLs '
4815 'has the potential to overload our build '
4816 'infrastructure. Try to upload these not during high '
4817 'load times (usually 11-3 Mountain View time). Email '
4818 'infra-dev@chromium.org with any questions.')
Takuto Ikuta51eca592019-02-14 19:40:52 +00004819 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4820 default=True,
4821 help='Sends your change to the CQ after an approval. Only '
4822 'works on repos that have the Auto-Submit label '
4823 'enabled')
Francois Dorayd42c6812017-05-30 15:10:20 -04004824 options, _ = parser.parse_args(args)
4825
4826 if not options.description_file:
4827 parser.error('No --description flag specified.')
4828
4829 def WrappedCMDupload(args):
4830 return CMDupload(OptionParser(), args)
4831
4832 return split_cl.SplitCl(options.description_file, options.comment_file,
Stephen Martiniscb326682018-08-29 21:06:30 +00004833 Changelist, WrappedCMDupload, options.dry_run,
Takuto Ikuta51eca592019-02-14 19:40:52 +00004834 options.cq_dry_run, options.enable_auto_submit)
Francois Dorayd42c6812017-05-30 15:10:20 -04004835
4836
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004837@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004838@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004839def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004840 """DEPRECATED: Used to commit the current changelist via git-svn."""
4841 message = ('git-cl no longer supports committing to SVN repositories via '
4842 'git-svn. You probably want to use `git cl land` instead.')
4843 print(message)
4844 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004845
4846
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01004847# Two special branches used by git cl land.
4848MERGE_BRANCH = 'git-cl-commit'
4849CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4850
4851
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004852@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004853@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004854def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004855 """Commits the current changelist via git.
4856
4857 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4858 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004859 """
4860 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4861 help='bypass upload presubmit hook')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004862 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004863 help="force yes to questions (don't prompt)")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04004864 parser.add_option('--parallel', action='store_true',
4865 help='Run all tests specified by input_api.RunTests in all '
4866 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004867 auth.add_auth_options(parser)
4868 (options, args) = parser.parse_args(args)
4869 auth_config = auth.extract_auth_config_from_options(options)
4870
4871 cl = Changelist(auth_config=auth_config)
4872
Robert Iannucci2e73d432018-03-14 01:10:47 -07004873 if not cl.IsGerrit():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004874 parser.error('Rietveld is not supported.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004875
Robert Iannucci2e73d432018-03-14 01:10:47 -07004876 if not cl.GetIssue():
4877 DieWithError('You must upload the change first to Gerrit.\n'
4878 ' If you would rather have `git cl land` upload '
4879 'automatically for you, see http://crbug.com/642759')
4880 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02004881 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004882
4883
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004884@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004885@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004886def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004887 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004888 parser.add_option('-b', dest='newbranch',
4889 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004890 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07004891 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004892 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07004893 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004894 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004895 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004896 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004897 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004898 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004899 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004900
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004901 group = optparse.OptionGroup(
4902 parser,
4903 'Options for continuing work on the current issue uploaded from a '
4904 'different clone (e.g. different machine). Must be used independently '
4905 'from the other options. No issue number should be specified, and the '
4906 'branch must have an issue number associated with it')
4907 group.add_option('--reapply', action='store_true', dest='reapply',
4908 help='Reset the branch and reapply the issue.\n'
4909 'CAUTION: This will undo any local changes in this '
4910 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004911
4912 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004913 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004914 parser.add_option_group(group)
4915
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004916 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004917 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004918 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004919 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004920 auth_config = auth.extract_auth_config_from_options(options)
4921
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004922 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004923 if options.newbranch:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004924 parser.error('--reapply works on the current branch only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004925 if len(args) > 0:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004926 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004927
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004928 cl = Changelist(auth_config=auth_config,
4929 codereview=options.forced_codereview)
4930 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004931 parser.error('Current branch must have an associated issue.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004932
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004933 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004934 if upstream is None:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004935 parser.error('No upstream branch specified. Cannot reset branch.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004936
4937 RunGit(['reset', '--hard', upstream])
4938 if options.pull:
4939 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004940
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004941 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4942 options.directory)
4943
4944 if len(args) != 1 or not args[0]:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004945 parser.error('Must specify issue number or URL.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004946
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004947 target_issue_arg = ParseIssueNumberArgument(args[0],
4948 options.forced_codereview)
4949 if not target_issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004950 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004951
4952 cl_kwargs = {
4953 'auth_config': auth_config,
4954 'codereview_host': target_issue_arg.hostname,
4955 'codereview': options.forced_codereview,
4956 }
4957 detected_codereview_from_url = False
4958 if target_issue_arg.codereview and not options.forced_codereview:
4959 detected_codereview_from_url = True
4960 cl_kwargs['codereview'] = target_issue_arg.codereview
4961 cl_kwargs['issue'] = target_issue_arg.issue
4962
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004963 # We don't want uncommitted changes mixed up with the patch.
4964 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004965 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004966
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004967 if options.newbranch:
4968 if options.force:
4969 RunGit(['branch', '-D', options.newbranch],
4970 stderr=subprocess2.PIPE, error_ok=True)
4971 RunGit(['new-branch', options.newbranch])
4972
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004973 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004974
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004975 if cl.IsGerrit():
4976 if options.reject:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004977 parser.error('--reject is not supported with Gerrit code review.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004978 if options.directory:
4979 parser.error('--directory is not supported with Gerrit codereview.')
4980
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004981 if detected_codereview_from_url:
4982 print('canonical issue/change URL: %s (type: %s)\n' %
4983 (cl.GetIssueURL(), target_issue_arg.codereview))
4984
4985 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07004986 options.nocommit, options.directory,
4987 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004988
4989
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004990def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004991 """Fetches the tree status and returns either 'open', 'closed',
4992 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004993 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004994 if url:
4995 status = urllib2.urlopen(url).read().lower()
4996 if status.find('closed') != -1 or status == '0':
4997 return 'closed'
4998 elif status.find('open') != -1 or status == '1':
4999 return 'open'
5000 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005001 return 'unset'
5002
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005003
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005004def GetTreeStatusReason():
5005 """Fetches the tree status from a json url and returns the message
5006 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005007 url = settings.GetTreeStatusUrl()
5008 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005009 connection = urllib2.urlopen(json_url)
5010 status = json.loads(connection.read())
5011 connection.close()
5012 return status['message']
5013
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005014
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005015@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005016def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005017 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005018 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005019 status = GetTreeStatus()
5020 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005021 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005022 return 2
5023
vapiera7fbd5a2016-06-16 09:17:49 -07005024 print('The tree is %s' % status)
5025 print()
5026 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005027 if status != 'open':
5028 return 1
5029 return 0
5030
5031
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005032@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00005033def CMDtry(parser, args):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005034 """Triggers tryjobs using either Buildbucket or CQ dry run."""
5035 group = optparse.OptionGroup(parser, 'Tryjob options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005036 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005037 '-b', '--bot', action='append',
5038 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5039 'times to specify multiple builders. ex: '
5040 '"-b win_rel -b win_layout". See '
5041 'the try server waterfall for the builders name and the tests '
5042 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005043 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005044 '-B', '--bucket', default='',
5045 help=('Buildbucket bucket to send the try requests.'))
5046 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005047 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005048 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005049 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005050 '-r', '--revision',
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005051 help='Revision to use for the tryjob; default: the revision will '
tandriif7b29d42016-10-07 08:45:41 -07005052 'be determined by the try recipe that builder runs, which usually '
5053 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005054 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005055 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005056 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005057 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005058 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005059 '--category', default='git_cl_try', help='Specify custom build category.')
5060 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005061 '--project',
5062 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005063 'in recipe to determine to which repository or directory to '
5064 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005065 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005066 '-p', '--property', dest='properties', action='append', default=[],
5067 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005068 'key2=value2 etc. The value will be treated as '
5069 'json if decodable, or as string otherwise. '
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005070 'NOTE: using this may make your tryjob not usable for CQ, '
5071 'which will then schedule another tryjob with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005072 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005073 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5074 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005075 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005076 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09005077 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005078 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09005079 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005080 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005081
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005082 if options.master and options.master.startswith('luci.'):
5083 parser.error(
5084 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005085 # Make sure that all properties are prop=value pairs.
5086 bad_params = [x for x in options.properties if '=' not in x]
5087 if bad_params:
5088 parser.error('Got properties with missing "=": %s' % bad_params)
5089
maruel@chromium.org15192402012-09-06 12:38:29 +00005090 if args:
5091 parser.error('Unknown arguments: %s' % args)
5092
Koji Ishii31c14782018-01-08 17:17:33 +09005093 cl = Changelist(auth_config=auth_config, issue=options.issue,
5094 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00005095 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005096 parser.error('Need to upload first.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005097
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005098 if cl.IsGerrit():
5099 # HACK: warm up Gerrit change detail cache to save on RPCs.
5100 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5101
tandriie113dfd2016-10-11 10:20:12 -07005102 error_message = cl.CannotTriggerTryJobReason()
5103 if error_message:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005104 parser.error('Can\'t trigger tryjobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005105
borenet6c0efe62016-10-19 08:13:29 -07005106 if options.bucket and options.master:
5107 parser.error('Only one of --bucket and --master may be used.')
5108
qyearsley1fdfcb62016-10-24 13:22:03 -07005109 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005110
qyearsleydd49f942016-10-28 11:57:22 -07005111 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5112 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005113 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005114 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005115 print('git cl try with no bots now defaults to CQ dry run.')
5116 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5117 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005118
borenet6c0efe62016-10-19 08:13:29 -07005119 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005120 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005121 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005122 'of bot requires an initial job from a parent (usually a builder). '
5123 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005124 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005125 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005126
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005127 patchset = cl.GetMostRecentPatchset()
tandrii568043b2016-10-11 07:49:18 -07005128 try:
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005129 _trigger_try_jobs(auth_config, cl, buckets, options, patchset)
tandrii568043b2016-10-11 07:49:18 -07005130 except BuildbucketResponseException as ex:
5131 print('ERROR: %s' % ex)
5132 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005133 return 0
5134
5135
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005136@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005137def CMDtry_results(parser, args):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00005138 """Prints info about results for tryjobs associated with the current CL."""
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005139 group = optparse.OptionGroup(parser, 'Tryjob results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005140 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005141 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005142 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005143 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005144 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005145 '--color', action='store_true', default=setup_color.IS_TTY,
5146 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005147 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005148 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5149 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005150 group.add_option(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005151 '--json', help=('Path of JSON output file to write tryjob results to,'
Stefan Zager1306bd02017-06-22 19:26:46 -07005152 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005153 parser.add_option_group(group)
5154 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07005155 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005156 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07005157 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005158 if args:
5159 parser.error('Unrecognized args: %s' % ' '.join(args))
5160
5161 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07005162 cl = Changelist(
5163 issue=options.issue, codereview=options.forced_codereview,
5164 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005165 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005166 parser.error('Need to upload first.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005167
tandrii221ab252016-10-06 08:12:04 -07005168 patchset = options.patchset
5169 if not patchset:
5170 patchset = cl.GetMostRecentPatchset()
5171 if not patchset:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005172 parser.error('Code review host doesn\'t know about issue %s. '
tandrii221ab252016-10-06 08:12:04 -07005173 'No access to issue or wrong issue number?\n'
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005174 'Either upload first, or pass --patchset explicitly.' %
tandrii221ab252016-10-06 08:12:04 -07005175 cl.GetIssue())
5176
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005177 try:
tandrii221ab252016-10-06 08:12:04 -07005178 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005179 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005180 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005181 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005182 if options.json:
5183 write_try_results_json(options.json, jobs)
5184 else:
5185 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005186 return 0
5187
5188
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005189@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005190@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005191def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005192 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005193 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005194 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005195 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005196
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005197 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005198 if args:
5199 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005200 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005201 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005202 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005203 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005204
5205 # Clear configured merge-base, if there is one.
5206 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005207 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005208 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005209 return 0
5210
5211
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005212@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00005213def CMDweb(parser, args):
5214 """Opens the current CL in the web browser."""
5215 _, args = parser.parse_args(args)
5216 if args:
5217 parser.error('Unrecognized args: %s' % ' '.join(args))
5218
5219 issue_url = Changelist().GetIssueURL()
5220 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005221 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005222 return 1
5223
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005224 # Redirect I/O before invoking browser to hide its output. For example, this
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005225 # allows us to hide the "Created new window in existing browser session."
5226 # message from Chrome. Based on https://stackoverflow.com/a/2323563.
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005227 saved_stdout = os.dup(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005228 saved_stderr = os.dup(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005229 os.close(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005230 os.close(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005231 os.open(os.devnull, os.O_RDWR)
5232 try:
5233 webbrowser.open(issue_url)
5234 finally:
5235 os.dup2(saved_stdout, 1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005236 os.dup2(saved_stderr, 2)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005237 return 0
5238
5239
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005240@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005241def CMDset_commit(parser, args):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00005242 """Sets the commit bit to trigger the CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005243 parser.add_option('-d', '--dry-run', action='store_true',
5244 help='trigger in dry run mode')
5245 parser.add_option('-c', '--clear', action='store_true',
5246 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005247 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005248 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005249 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005250 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005251 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005252 if args:
5253 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005254 if options.dry_run and options.clear:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005255 parser.error('Only one of --dry-run and --clear are allowed.')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005256
iannuccie53c9352016-08-17 14:40:40 -07005257 cl = Changelist(auth_config=auth_config, issue=options.issue,
5258 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005259 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005260 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005261 elif options.dry_run:
5262 state = _CQState.DRY_RUN
5263 else:
5264 state = _CQState.COMMIT
5265 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005266 parser.error('Must upload the issue first.')
tandrii9de9ec62016-07-13 03:01:59 -07005267 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005268 return 0
5269
5270
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005271@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005272def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005273 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005274 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005275 auth.add_auth_options(parser)
5276 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005277 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005278 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005279 if args:
5280 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005281 cl = Changelist(auth_config=auth_config, issue=options.issue,
5282 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005283 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005284 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005285 DieWithError('ERROR: No issue to close.')
groby@chromium.org411034a2013-02-26 15:12:01 +00005286 cl.CloseIssue()
5287 return 0
5288
5289
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005290@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005291def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005292 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005293 parser.add_option(
5294 '--stat',
5295 action='store_true',
5296 dest='stat',
5297 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005298 auth.add_auth_options(parser)
5299 options, args = parser.parse_args(args)
5300 auth_config = auth.extract_auth_config_from_options(options)
5301 if args:
5302 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005303
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005304 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005305 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005306 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005307 if not issue:
5308 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005309
Aaron Gablea718c3e2017-08-28 17:47:28 -07005310 base = cl._GitGetBranchConfigValue('last-upload-hash')
5311 if not base:
5312 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5313 if not base:
5314 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5315 revision_info = detail['revisions'][detail['current_revision']]
5316 fetch_info = revision_info['fetch']['http']
5317 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5318 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005319
Aaron Gablea718c3e2017-08-28 17:47:28 -07005320 cmd = ['git', 'diff']
5321 if options.stat:
5322 cmd.append('--stat')
5323 cmd.append(base)
5324 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005325
5326 return 0
5327
5328
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005329@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005330def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005331 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005332 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005333 '--ignore-current',
5334 action='store_true',
5335 help='Ignore the CL\'s current reviewers and start from scratch.')
5336 parser.add_option(
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005337 '--ignore-self',
5338 action='store_true',
5339 help='Do not consider CL\'s author as an owners.')
5340 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005341 '--no-color',
5342 action='store_true',
5343 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005344 parser.add_option(
5345 '--batch',
5346 action='store_true',
5347 help='Do not run interactively, just suggest some')
Yang Guo6e269a02019-06-26 11:17:02 +00005348 # TODO: Consider moving this to another command, since other
5349 # git-cl owners commands deal with owners for a given CL.
5350 parser.add_option(
5351 '--show-all',
5352 action='store_true',
5353 help='Show all owners for a particular file')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005354 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005355 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005356 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005357
5358 author = RunGit(['config', 'user.email']).strip() or None
5359
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005360 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005361
Yang Guo6e269a02019-06-26 11:17:02 +00005362 if options.show_all:
5363 for arg in args:
5364 base_branch = cl.GetCommonAncestorWithUpstream()
5365 change = cl.GetChange(base_branch, None)
5366 database = owners.Database(change.RepositoryRoot(), file, os.path)
5367 database.load_data_needed_for([arg])
5368 print('Owners for %s:' % arg)
5369 for owner in sorted(database.all_possible_owners([arg], None)):
5370 print(' - %s' % owner)
5371 return 0
5372
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005373 if args:
5374 if len(args) > 1:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005375 parser.error('Unknown args.')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005376 base_branch = args[0]
5377 else:
5378 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005379 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005380
5381 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005382 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5383
5384 if options.batch:
5385 db = owners.Database(change.RepositoryRoot(), file, os.path)
5386 print('\n'.join(db.reviewers_for(affected_files, author)))
5387 return 0
5388
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005389 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005390 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005391 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005392 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005393 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005394 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005395 disable_color=options.no_color,
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005396 override_files=change.OriginalOwnersFiles(),
5397 ignore_author=options.ignore_self).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005398
5399
Aiden Bennerc08566e2018-10-03 17:52:42 +00005400def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005401 """Generates a diff command."""
5402 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005403 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
5404
Aiden Benner6c18a1a2018-11-23 20:18:23 +00005405 if allow_prefix:
5406 # explicitly setting --src-prefix and --dst-prefix is necessary in the
5407 # case that diff.noprefix is set in the user's git config.
5408 diff_cmd += ['--src-prefix=a/', '--dst-prefix=b/']
5409 else:
Aiden Bennerc08566e2018-10-03 17:52:42 +00005410 diff_cmd += ['--no-prefix']
5411
5412 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005413
5414 if args:
5415 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005416 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005417 diff_cmd.append(arg)
5418 else:
5419 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005420
5421 return diff_cmd
5422
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005423
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005424def MatchingFileType(file_name, extensions):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00005425 """Returns True if the file name ends with one of the given extensions."""
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005426 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005427
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005428
enne@chromium.org555cfe42014-01-29 18:21:39 +00005429@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005430@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005431def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005432 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005433 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005434 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005435 parser.add_option('--full', action='store_true',
5436 help='Reformat the full content of all touched files')
5437 parser.add_option('--dry-run', action='store_true',
5438 help='Don\'t modify any file on disk.')
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005439 parser.add_option(
5440 '--python',
5441 action='store_true',
5442 default=None,
5443 help='Enables python formatting on all python files.')
5444 parser.add_option(
5445 '--no-python',
5446 action='store_true',
5447 dest='python',
5448 help='Disables python formatting on all python files. '
5449 'Takes precedence over --python. '
5450 'If neither --python or --no-python are set, python '
5451 'files that have a .style.yapf file in an ancestor '
5452 'directory will be formatted.')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005453 parser.add_option('--js', action='store_true',
5454 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005455 parser.add_option('--diff', action='store_true',
5456 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005457 parser.add_option('--presubmit', action='store_true',
5458 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005459 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005460
Daniel Chengc55eecf2016-12-30 03:11:02 -08005461 # Normalize any remaining args against the current path, so paths relative to
5462 # the current directory are still resolved as expected.
5463 args = [os.path.join(os.getcwd(), arg) for arg in args]
5464
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005465 # git diff generates paths against the root of the repository. Change
5466 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005467 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005468 if rel_base_path:
5469 os.chdir(rel_base_path)
5470
digit@chromium.org29e47272013-05-17 17:01:46 +00005471 # Grab the merge-base commit, i.e. the upstream commit of the current
5472 # branch when it was created or the last time it was rebased. This is
5473 # to cover the case where the user may have called "git fetch origin",
5474 # moving the origin branch to a newer commit, but hasn't rebased yet.
5475 upstream_commit = None
5476 cl = Changelist()
5477 upstream_branch = cl.GetUpstreamBranch()
5478 if upstream_branch:
5479 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5480 upstream_commit = upstream_commit.strip()
5481
5482 if not upstream_commit:
5483 DieWithError('Could not find base commit for this branch. '
5484 'Are you in detached state?')
5485
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005486 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5487 diff_output = RunGit(changed_files_cmd)
5488 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005489 # Filter out files deleted by this CL
5490 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005491
Christopher Lamc5ba6922017-01-24 11:19:14 +11005492 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00005493 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11005494
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005495 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5496 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5497 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005498 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005499
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005500 top_dir = os.path.normpath(
5501 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5502
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005503 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5504 # formatted. This is used to block during the presubmit.
5505 return_value = 0
5506
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005507 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005508 # Locate the clang-format binary in the checkout
5509 try:
5510 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005511 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005512 DieWithError(e)
5513
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005514 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005515 cmd = [clang_format_tool]
5516 if not opts.dry_run and not opts.diff:
5517 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005518 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005519 if opts.diff:
5520 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005521 else:
5522 env = os.environ.copy()
5523 env['PATH'] = str(os.path.dirname(clang_format_tool))
5524 try:
5525 script = clang_format.FindClangFormatScriptInChromiumTree(
5526 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005527 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005528 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005529
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005530 cmd = [sys.executable, script, '-p0']
5531 if not opts.dry_run and not opts.diff:
5532 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005533
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005534 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5535 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005536
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005537 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5538 if opts.diff:
5539 sys.stdout.write(stdout)
5540 if opts.dry_run and len(stdout) > 0:
5541 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005542
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005543 # Similar code to above, but using yapf on .py files rather than clang-format
5544 # on C/C++ files
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005545 py_explicitly_disabled = opts.python is not None and not opts.python
5546 if python_diff_files and not py_explicitly_disabled:
Aiden Bennere47ac152018-11-20 16:44:03 +00005547 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
5548 yapf_tool = os.path.join(depot_tools_path, 'yapf')
5549 if sys.platform.startswith('win'):
5550 yapf_tool += '.bat'
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005551
Aiden Bennerc08566e2018-10-03 17:52:42 +00005552 # If we couldn't find a yapf file we'll default to the chromium style
5553 # specified in depot_tools.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005554 chromium_default_yapf_style = os.path.join(depot_tools_path,
5555 YAPF_CONFIG_FILENAME)
Aiden Bennerc08566e2018-10-03 17:52:42 +00005556 # Used for caching.
5557 yapf_configs = {}
5558 for f in python_diff_files:
5559 # Find the yapf style config for the current file, defaults to depot
5560 # tools default.
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005561 _FindYapfConfigFile(f, yapf_configs, top_dir)
5562
5563 # Turn on python formatting by default if a yapf config is specified.
5564 # This breaks in the case of this repo though since the specified
5565 # style file is also the global default.
5566 if opts.python is None:
5567 filtered_py_files = []
5568 for f in python_diff_files:
5569 if _FindYapfConfigFile(f, yapf_configs, top_dir) is not None:
5570 filtered_py_files.append(f)
5571 else:
5572 filtered_py_files = python_diff_files
5573
5574 # Note: yapf still seems to fix indentation of the entire file
5575 # even if line ranges are specified.
5576 # See https://github.com/google/yapf/issues/499
5577 if not opts.full and filtered_py_files:
5578 py_line_diffs = _ComputeDiffLineRanges(filtered_py_files, upstream_commit)
5579
5580 for f in filtered_py_files:
5581 yapf_config = _FindYapfConfigFile(f, yapf_configs, top_dir)
5582 if yapf_config is None:
5583 yapf_config = chromium_default_yapf_style
Aiden Bennerc08566e2018-10-03 17:52:42 +00005584
5585 cmd = [yapf_tool, '--style', yapf_config, f]
5586
5587 has_formattable_lines = False
5588 if not opts.full:
5589 # Only run yapf over changed line ranges.
5590 for diff_start, diff_len in py_line_diffs[f]:
5591 diff_end = diff_start + diff_len - 1
5592 # Yapf errors out if diff_end < diff_start but this
5593 # is a valid line range diff for a removal.
5594 if diff_end >= diff_start:
5595 has_formattable_lines = True
5596 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
5597 # If all line diffs were removals we have nothing to format.
5598 if not has_formattable_lines:
5599 continue
5600
5601 if opts.diff or opts.dry_run:
5602 cmd += ['--diff']
5603 # Will return non-zero exit code if non-empty diff.
5604 stdout = RunCommand(cmd, error_ok=True, cwd=top_dir)
5605 if opts.diff:
5606 sys.stdout.write(stdout)
5607 elif len(stdout) > 0:
5608 return_value = 2
5609 else:
5610 cmd += ['-i']
5611 RunCommand(cmd, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005612
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005613 # Dart's formatter does not have the nice property of only operating on
5614 # modified chunks, so hard code full.
5615 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005616 try:
5617 command = [dart_format.FindDartFmtToolInChromiumTree()]
5618 if not opts.dry_run and not opts.diff:
5619 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005620 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005621
ppi@chromium.org6593d932016-03-03 15:41:15 +00005622 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005623 if opts.dry_run and stdout:
5624 return_value = 2
5625 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005626 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5627 'found in this checkout. Files in other languages are still '
5628 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005629
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005630 # Format GN build files. Always run on full build files for canonical form.
5631 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005632 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005633 if opts.dry_run or opts.diff:
5634 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005635 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005636 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5637 shell=sys.platform == 'win32',
5638 cwd=top_dir)
5639 if opts.dry_run and gn_ret == 2:
5640 return_value = 2 # Not formatted.
5641 elif opts.diff and gn_ret == 2:
5642 # TODO this should compute and print the actual diff.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005643 print('This change has GN build file diff for ' + gn_diff_file)
brettw4b8ed592016-08-05 16:19:12 -07005644 elif gn_ret != 0:
5645 # For non-dry run cases (and non-2 return values for dry-run), a
5646 # nonzero error code indicates a failure, probably because the file
5647 # doesn't parse.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005648 DieWithError('gn format failed on ' + gn_diff_file +
5649 '\nTry running `gn format` on this file manually.')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005650
Ilya Shermane081cbe2017-08-15 17:51:04 -07005651 # Skip the metrics formatting from the global presubmit hook. These files have
5652 # a separate presubmit hook that issues an error if the files need formatting,
5653 # whereas the top-level presubmit script merely issues a warning. Formatting
5654 # these files is somewhat slow, so it's important not to duplicate the work.
5655 if not opts.presubmit:
5656 for xml_dir in GetDirtyMetricsDirs(diff_files):
5657 tool_dir = os.path.join(top_dir, xml_dir)
5658 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5659 if opts.dry_run or opts.diff:
5660 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07005661 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07005662 if opts.diff:
5663 sys.stdout.write(stdout)
5664 if opts.dry_run and stdout:
5665 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005666
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005667 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005668
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005669
Steven Holte2e664bf2017-04-21 13:10:47 -07005670def GetDirtyMetricsDirs(diff_files):
5671 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5672 metrics_xml_dirs = [
5673 os.path.join('tools', 'metrics', 'actions'),
5674 os.path.join('tools', 'metrics', 'histograms'),
5675 os.path.join('tools', 'metrics', 'rappor'),
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005676 os.path.join('tools', 'metrics', 'ukm'),
5677 ]
Steven Holte2e664bf2017-04-21 13:10:47 -07005678 for xml_dir in metrics_xml_dirs:
5679 if any(file.startswith(xml_dir) for file in xml_diff_files):
5680 yield xml_dir
5681
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005682
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005683@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005684@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005685def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005686 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005687 _, args = parser.parse_args(args)
5688
5689 if len(args) != 1:
5690 parser.print_help()
5691 return 1
5692
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005693 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005694 if not issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005695 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005696
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005697 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005698
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005699 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005700 output = RunGit(['config', '--local', '--get-regexp',
5701 r'branch\..*\.%s' % issueprefix],
5702 error_ok=True)
5703 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005704 if issue == target_issue:
5705 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005706
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005707 branches = []
5708 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005709 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005710 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005711 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005712 return 1
5713 if len(branches) == 1:
5714 RunGit(['checkout', branches[0]])
5715 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005716 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005717 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005718 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005719 which = raw_input('Choose by index: ')
5720 try:
5721 RunGit(['checkout', branches[int(which)]])
5722 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005723 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005724 return 1
5725
5726 return 0
5727
5728
maruel@chromium.org29404b52014-09-08 22:58:00 +00005729def CMDlol(parser, args):
5730 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005731 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005732 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5733 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5734 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005735 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005736 return 0
5737
5738
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005739class OptionParser(optparse.OptionParser):
5740 """Creates the option parse and add --verbose support."""
5741 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005742 optparse.OptionParser.__init__(
5743 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005744 self.add_option(
5745 '-v', '--verbose', action='count', default=0,
5746 help='Use 2 times for more debugging info')
5747
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005748 def parse_args(self, args=None, _values=None):
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00005749 try:
5750 return self._parse_args(args)
5751 finally:
5752 # Regardless of success or failure of args parsing, we want to report
5753 # metrics, but only after logging has been initialized (if parsing
5754 # succeeded).
5755 global settings
5756 settings = Settings()
5757
5758 if not metrics.DISABLE_METRICS_COLLECTION:
5759 # GetViewVCUrl ultimately calls logging method.
5760 project_url = settings.GetViewVCUrl().strip('/+')
5761 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
5762 metrics.collector.add('project_urls', [project_url])
5763
5764 def _parse_args(self, args=None):
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005765 # Create an optparse.Values object that will store only the actual passed
5766 # options, without the defaults.
5767 actual_options = optparse.Values()
5768 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
5769 # Create an optparse.Values object with the default options.
5770 options = optparse.Values(self.get_default_values().__dict__)
5771 # Update it with the options passed by the user.
5772 options._update_careful(actual_options.__dict__)
5773 # Store the options passed by the user in an _actual_options attribute.
5774 # We store only the keys, and not the values, since the values can contain
5775 # arbitrary information, which might be PII.
5776 metrics.collector.add('arguments', actual_options.__dict__.keys())
5777
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005778 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005779 logging.basicConfig(
5780 level=levels[min(options.verbose, len(levels) - 1)],
5781 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5782 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00005783
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005784 return options, args
5785
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005786
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005787def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005788 if sys.hexversion < 0x02060000:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005789 print('\nYour Python version %s is unsupported, please upgrade.\n' %
vapiera7fbd5a2016-06-16 09:17:49 -07005790 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005791 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005792
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005793 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005794 dispatcher = subcommand.CommandDispatcher(__name__)
5795 try:
5796 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005797 except auth.AuthenticationError as e:
5798 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005799 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005800 if e.code != 500:
5801 raise
5802 DieWithError(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005803 ('App Engine is misbehaving and returned HTTP %d, again. Keep faith '
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005804 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005805 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005806
5807
5808if __name__ == '__main__':
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005809 # These affect sys.stdout, so do it outside of main() to simplify mocks in
5810 # the unit tests.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005811 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005812 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00005813 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00005814 sys.exit(main(sys.argv[1:]))