blob: b1eafde67ac5f7670d980849b5faa8335f222433 [file] [log] [blame]
Josip Sokcevic4de5dea2022-03-23 21:15:14 +00001#!/usr/bin/env vpython3
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
thakis@chromium.org3421c992014-11-02 02:20:32 +000012import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000013import collections
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +010014import datetime
Brian Sheedyb4307d52019-12-02 19:18:17 +000015import fnmatch
Edward Lemur202c5592019-10-21 22:44:52 +000016import httplib2
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +010017import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000018import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000020import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import optparse
22import os
23import re
Andrii Shyshkalov353637c2017-03-14 16:52:18 +010024import shutil
ukai@chromium.org78c4b982012-02-14 02:20:26 +000025import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000026import sys
Aaron Gable9a03ae02017-11-03 11:31:07 -070027import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028import textwrap
Edward Lemurfec80c42018-11-01 23:14:14 +000029import time
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000030import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000031import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000032import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000033
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000034from third_party import colorama
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000035import auth
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000036import clang_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000037import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000038import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000039import gerrit_util
iannucci@chromium.org9e849272014-04-04 00:31:55 +000040import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000041import git_footers
Edward Lemur85153282020-02-14 22:06:29 +000042import git_new_branch
Edward Lemur5ba1e9c2018-07-23 18:19:02 +000043import metrics
Edward Lesmes93277a72018-10-18 22:04:26 +000044import metrics_utils
Edward Lesmeseeca9c62020-11-20 00:00:17 +000045import owners_client
iannucci@chromium.org9e849272014-04-04 00:31:55 +000046import owners_finder
Lei Zhangb8c62cf2020-07-15 20:09:37 +000047import presubmit_canned_checks
maruel@chromium.org2a74d372011-03-29 19:05:50 +000048import presubmit_support
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +000049import rustfmt
maruel@chromium.org2a74d372011-03-29 19:05:50 +000050import scm
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +000051import setup_color
Francois Dorayd42c6812017-05-30 15:10:20 -040052import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000053import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000054import subprocess2
Olivier Robin0a6b5442022-04-07 07:25:04 +000055import swift_format
maruel@chromium.org2a74d372011-03-29 19:05:50 +000056import watchlists
57
Edward Lemur79d4f992019-11-11 23:49:02 +000058from third_party import six
59from six.moves import urllib
60
61
62if sys.version_info.major == 3:
63 basestring = (str,) # pylint: disable=redefined-builtin
64
Edward Lemurb9830242019-10-30 22:19:20 +000065
tandrii7400cf02016-06-21 08:48:07 -070066__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000067
Edward Lemur0f58ae42019-04-30 17:24:12 +000068# Traces for git push will be stored in a traces directory inside the
69# depot_tools checkout.
70DEPOT_TOOLS = os.path.dirname(os.path.abspath(__file__))
71TRACES_DIR = os.path.join(DEPOT_TOOLS, 'traces')
Edward Lemur227d5102020-02-25 23:45:35 +000072PRESUBMIT_SUPPORT = os.path.join(DEPOT_TOOLS, 'presubmit_support.py')
Edward Lemur0f58ae42019-04-30 17:24:12 +000073
74# When collecting traces, Git hashes will be reduced to 6 characters to reduce
75# the size after compression.
76GIT_HASH_RE = re.compile(r'\b([a-f0-9]{6})[a-f0-9]{34}\b', flags=re.I)
77# Used to redact the cookies from the gitcookies file.
78GITCOOKIES_REDACT_RE = re.compile(r'1/.*')
79
Edward Lemurd4d1ba42019-09-20 21:46:37 +000080MAX_ATTEMPTS = 3
81
Edward Lemur1b52d872019-05-09 21:12:12 +000082# The maximum number of traces we will keep. Multiplied by 3 since we store
83# 3 files per trace.
84MAX_TRACES = 3 * 10
Edward Lemur5737f022019-05-17 01:24:00 +000085# Message to be displayed to the user to inform where to find the traces for a
86# git-cl upload execution.
Edward Lemur0f58ae42019-04-30 17:24:12 +000087TRACES_MESSAGE = (
Edward Lemur1b52d872019-05-09 21:12:12 +000088'\n'
Edward Lemur5737f022019-05-17 01:24:00 +000089'The traces of this git-cl execution have been recorded at:\n'
Edward Lemur1b52d872019-05-09 21:12:12 +000090' %(trace_name)s-traces.zip\n'
Edward Lemur5737f022019-05-17 01:24:00 +000091'Copies of your gitcookies file and git config have been recorded at:\n'
92' %(trace_name)s-git-info.zip\n')
Edward Lemur1b52d872019-05-09 21:12:12 +000093# Format of the message to be stored as part of the traces to give developers a
94# better context when they go through traces.
95TRACES_README_FORMAT = (
96'Date: %(now)s\n'
97'\n'
98'Change: https://%(gerrit_host)s/q/%(change_id)s\n'
99'Title: %(title)s\n'
100'\n'
101'%(description)s\n'
102'\n'
103'Execution time: %(execution_time)s\n'
104'Exit code: %(exit_code)s\n') + TRACES_MESSAGE
Edward Lemur0f58ae42019-04-30 17:24:12 +0000105
Aaron Gable1bc7bfe2016-12-19 10:08:14 -0800106POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
Henrique Ferreiroff249622019-11-28 23:19:29 +0000107DESCRIPTION_BACKUP_FILE = '.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +0000108REFS_THAT_ALIAS_TO_OTHER_REFS = {
Josip Sokcevic7e133ff2021-07-13 17:44:53 +0000109 'refs/remotes/origin/lkgr': 'refs/remotes/origin/main',
110 'refs/remotes/origin/lkcr': 'refs/remotes/origin/main',
rmistry@google.comc68112d2015-03-03 12:48:06 +0000111}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000112
Josip Sokcevicc39ab992020-09-24 20:09:15 +0000113DEFAULT_OLD_BRANCH = 'refs/remotes/origin/master'
114DEFAULT_NEW_BRANCH = 'refs/remotes/origin/main'
115
thestig@chromium.org44202a22014-03-11 19:22:18 +0000116# Valid extensions for files we want to lint.
117DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
118DEFAULT_LINT_IGNORE_REGEX = r"$^"
119
Aiden Bennerc08566e2018-10-03 17:52:42 +0000120# File name for yapf style config files.
121YAPF_CONFIG_FILENAME = '.style.yapf'
122
Edward Lesmes50da7702020-03-30 19:23:43 +0000123# The issue, patchset and codereview server are stored on git config for each
124# branch under branch.<branch-name>.<config-key>.
125ISSUE_CONFIG_KEY = 'gerritissue'
126PATCHSET_CONFIG_KEY = 'gerritpatchset'
127CODEREVIEW_SERVER_CONFIG_KEY = 'gerritserver'
Gavin Makbe2e9262022-11-08 23:41:55 +0000128# When using squash workflow, _CMDUploadChange doesn't simply push the commit(s)
129# you make to Gerrit. Instead, it creates a new commit object that contains all
130# changes you've made, diffed against a parent/merge base.
131# This is the hash of the new squashed commit and you can find this on Gerrit.
132GERRIT_SQUASH_HASH_CONFIG_KEY = 'gerritsquashhash'
133# This is the latest uploaded local commit hash.
134LAST_UPLOAD_HASH_CONFIG_KEY = 'last-upload-hash'
Edward Lesmes50da7702020-03-30 19:23:43 +0000135
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000136# Shortcut since it quickly becomes repetitive.
maruel@chromium.org2e23ce32013-05-07 12:42:28 +0000137Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +0000138
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000139# Initialized in main()
140settings = None
141
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100142# Used by tests/git_cl_test.py to add extra logging.
143# Inside the weirdly failing test, add this:
144# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700145# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100146_IS_BEING_TESTED = False
147
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000148_GOOGLESOURCE = 'googlesource.com'
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000149
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +0000150_KNOWN_GERRIT_TO_SHORT_URLS = {
151 'https://chrome-internal-review.googlesource.com': 'https://crrev.com/i',
152 'https://chromium-review.googlesource.com': 'https://crrev.com/c',
153}
Andrii Shyshkalov8aebb602020-04-16 22:10:27 +0000154assert len(_KNOWN_GERRIT_TO_SHORT_URLS) == len(
155 set(_KNOWN_GERRIT_TO_SHORT_URLS.values())), 'must have unique values'
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +0000156
157
Joanna Wang18de1f62023-01-21 01:24:24 +0000158# Maximum number of branches in a stack that can be traversed and uploaded
159# at once. Picked arbitrarily.
160_MAX_STACKED_BRANCHES_UPLOAD = 20
161
162
Josip Sokcevicf736cab2020-10-20 23:41:38 +0000163class GitPushError(Exception):
164 pass
165
166
Christopher Lamf732cd52017-01-24 12:40:11 +1100167def DieWithError(message, change_desc=None):
168 if change_desc:
169 SaveDescriptionBackup(change_desc)
Josip Sokcevic953278a2020-02-28 19:46:36 +0000170 print('\n ** Content of CL description **\n' +
171 '='*72 + '\n' +
172 change_desc.description + '\n' +
173 '='*72 + '\n')
Christopher Lamf732cd52017-01-24 12:40:11 +1100174
vapiera7fbd5a2016-06-16 09:17:49 -0700175 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000176 sys.exit(1)
177
178
Christopher Lamf732cd52017-01-24 12:40:11 +1100179def SaveDescriptionBackup(change_desc):
Henrique Ferreiro5ae48172019-11-29 16:14:42 +0000180 backup_path = os.path.join(DEPOT_TOOLS, DESCRIPTION_BACKUP_FILE)
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +0000181 print('\nsaving CL description to %s\n' % backup_path)
sokcevic07152802021-08-18 00:06:34 +0000182 with open(backup_path, 'wb') as backup_file:
183 backup_file.write(change_desc.description.encode('utf-8'))
Christopher Lamf732cd52017-01-24 12:40:11 +1100184
185
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000186def GetNoGitPagerEnv():
187 env = os.environ.copy()
188 # 'cat' is a magical git string that disables pagers on all platforms.
189 env['GIT_PAGER'] = 'cat'
190 return env
191
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000192
bsep@chromium.org627d9002016-04-29 00:00:52 +0000193def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000194 try:
Edward Lemur79d4f992019-11-11 23:49:02 +0000195 stdout = subprocess2.check_output(args, shell=shell, **kwargs)
196 return stdout.decode('utf-8', 'replace')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000197 except subprocess2.CalledProcessError as e:
198 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000199 if not error_ok:
Alan Cutter594fd332020-07-21 23:55:27 +0000200 message = error_message or e.stdout.decode('utf-8', 'replace') or ''
201 DieWithError('Command "%s" failed.\n%s' % (' '.join(args), message))
Josip Sokcevic673e8ed2021-10-27 23:46:18 +0000202 out = e.stdout.decode('utf-8', 'replace')
203 if e.stderr:
204 out += e.stderr.decode('utf-8', 'replace')
205 return out
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000206
207
208def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000209 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000210 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000211
212
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000213def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000214 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700215 if suppress_stderr:
Edward Lesmescf06cad2020-12-14 22:03:23 +0000216 stderr = subprocess2.DEVNULL
tandrii5d48c322016-08-18 16:19:37 -0700217 else:
218 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000219 try:
tandrii5d48c322016-08-18 16:19:37 -0700220 (out, _), code = subprocess2.communicate(['git'] + args,
221 env=GetNoGitPagerEnv(),
222 stdout=subprocess2.PIPE,
223 stderr=stderr)
Edward Lemur79d4f992019-11-11 23:49:02 +0000224 return code, out.decode('utf-8', 'replace')
tandrii5d48c322016-08-18 16:19:37 -0700225 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900226 logging.debug('Failed running %s', ['git'] + args)
Edward Lemur79d4f992019-11-11 23:49:02 +0000227 return e.returncode, e.stdout.decode('utf-8', 'replace')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000228
229
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000230def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000231 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000232 return RunGitWithCode(args, suppress_stderr=True)[1]
233
234
tandrii2a16b952016-10-19 07:09:44 -0700235def time_sleep(seconds):
236 # Use this so that it can be mocked in tests without interfering with python
237 # system machinery.
tandrii2a16b952016-10-19 07:09:44 -0700238 return time.sleep(seconds)
239
240
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000241def time_time():
242 # Use this so that it can be mocked in tests without interfering with python
243 # system machinery.
244 return time.time()
245
246
Edward Lemur1b52d872019-05-09 21:12:12 +0000247def datetime_now():
248 # Use this so that it can be mocked in tests without interfering with python
249 # system machinery.
250 return datetime.datetime.now()
251
252
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100253def confirm_or_exit(prefix='', action='confirm'):
254 """Asks user to press enter to continue or press Ctrl+C to abort."""
255 if not prefix or prefix.endswith('\n'):
256 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100257 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100258 mid = ' Press'
259 elif prefix.endswith(' '):
260 mid = 'press'
261 else:
262 mid = ' press'
Edward Lesmesae3586b2020-03-23 21:21:14 +0000263 gclient_utils.AskForData(
264 '%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100265
266
267def ask_for_explicit_yes(prompt):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +0000268 """Returns whether user typed 'y' or 'yes' to confirm the given prompt."""
Edward Lesmesae3586b2020-03-23 21:21:14 +0000269 result = gclient_utils.AskForData(prompt + ' [Yes/No]: ').lower()
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100270 while True:
271 if 'yes'.startswith(result):
272 return True
273 if 'no'.startswith(result):
274 return False
Edward Lesmesae3586b2020-03-23 21:21:14 +0000275 result = gclient_utils.AskForData('Please, type yes or no: ').lower()
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100276
277
machenbach@chromium.org45453142015-09-15 08:45:22 +0000278def _get_properties_from_options(options):
Quinten Yearsleya19d3532019-09-30 21:54:39 +0000279 prop_list = getattr(options, 'properties', [])
280 properties = dict(x.split('=', 1) for x in prop_list)
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000281 for key, val in properties.items():
machenbach@chromium.org45453142015-09-15 08:45:22 +0000282 try:
283 properties[key] = json.loads(val)
284 except ValueError:
285 pass # If a value couldn't be evaluated, treat it as a string.
286 return properties
287
288
Edward Lemur4c707a22019-09-24 21:13:43 +0000289def _call_buildbucket(http, buildbucket_host, method, request):
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000290 """Calls a buildbucket v2 method and returns the parsed json response."""
291 headers = {
292 'Accept': 'application/json',
293 'Content-Type': 'application/json',
294 }
295 request = json.dumps(request)
296 url = 'https://%s/prpc/buildbucket.v2.Builds/%s' % (buildbucket_host, method)
297
298 logging.info('POST %s with %s' % (url, request))
299
300 attempts = 1
301 time_to_sleep = 1
302 while True:
303 response, content = http.request(url, 'POST', body=request, headers=headers)
304 if response.status == 200:
305 return json.loads(content[4:])
306 if attempts >= MAX_ATTEMPTS or 400 <= response.status < 500:
307 msg = '%s error when calling POST %s with %s: %s' % (
308 response.status, url, request, content)
309 raise BuildbucketResponseException(msg)
310 logging.debug(
311 '%s error when calling POST %s with %s. '
312 'Sleeping for %d seconds and retrying...' % (
313 response.status, url, request, time_to_sleep))
314 time.sleep(time_to_sleep)
315 time_to_sleep *= 2
316 attempts += 1
317
318 assert False, 'unreachable'
319
320
Edward Lemur6215c792019-10-03 21:59:05 +0000321def _parse_bucket(raw_bucket):
322 legacy = True
323 project = bucket = None
324 if '/' in raw_bucket:
325 legacy = False
326 project, bucket = raw_bucket.split('/', 1)
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000327 # Assume luci.<project>.<bucket>.
Edward Lemur6215c792019-10-03 21:59:05 +0000328 elif raw_bucket.startswith('luci.'):
329 project, bucket = raw_bucket[len('luci.'):].split('.', 1)
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000330 # Otherwise, assume prefix is also the project name.
Edward Lemur6215c792019-10-03 21:59:05 +0000331 elif '.' in raw_bucket:
332 project = raw_bucket.split('.')[0]
333 bucket = raw_bucket
334 # Legacy buckets.
Edward Lemur45768512020-03-02 19:03:14 +0000335 if legacy and project and bucket:
Edward Lemur6215c792019-10-03 21:59:05 +0000336 print('WARNING Please use %s/%s to specify the bucket.' % (project, bucket))
337 return project, bucket
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000338
339
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000340def _canonical_git_googlesource_host(host):
341 """Normalizes Gerrit hosts (with '-review') to Git host."""
342 assert host.endswith(_GOOGLESOURCE)
343 # Prefix doesn't include '.' at the end.
344 prefix = host[:-(1 + len(_GOOGLESOURCE))]
345 if prefix.endswith('-review'):
346 prefix = prefix[:-len('-review')]
347 return prefix + '.' + _GOOGLESOURCE
348
349
350def _canonical_gerrit_googlesource_host(host):
351 git_host = _canonical_git_googlesource_host(host)
352 prefix = git_host.split('.', 1)[0]
353 return prefix + '-review.' + _GOOGLESOURCE
354
355
356def _get_counterpart_host(host):
357 assert host.endswith(_GOOGLESOURCE)
358 git = _canonical_git_googlesource_host(host)
359 gerrit = _canonical_gerrit_googlesource_host(git)
360 return git if gerrit == host else gerrit
361
362
Quinten Yearsley777660f2020-03-04 23:37:06 +0000363def _trigger_tryjobs(changelist, jobs, options, patchset):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000364 """Sends a request to Buildbucket to trigger tryjobs for a changelist.
qyearsley1fdfcb62016-10-24 13:22:03 -0700365
366 Args:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000367 changelist: Changelist that the tryjobs are associated with.
Edward Lemur45768512020-03-02 19:03:14 +0000368 jobs: A list of (project, bucket, builder).
qyearsley1fdfcb62016-10-24 13:22:03 -0700369 options: Command-line options.
370 """
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000371 print('Scheduling jobs on:')
Edward Lemur45768512020-03-02 19:03:14 +0000372 for project, bucket, builder in jobs:
373 print(' %s/%s: %s' % (project, bucket, builder))
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000374 print('To see results here, run: git cl try-results')
375 print('To see results in browser, run: git cl web')
tandriide281ae2016-10-12 06:02:30 -0700376
Quinten Yearsley777660f2020-03-04 23:37:06 +0000377 requests = _make_tryjob_schedule_requests(changelist, jobs, options, patchset)
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000378 if not requests:
379 return
380
Edward Lemur5b929a42019-10-21 17:57:39 +0000381 http = auth.Authenticator().authorize(httplib2.Http())
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000382 http.force_exception_to_status_code = True
383
384 batch_request = {'requests': requests}
385 batch_response = _call_buildbucket(
386 http, options.buildbucket_host, 'Batch', batch_request)
387
388 errors = [
389 ' ' + response['error']['message']
390 for response in batch_response.get('responses', [])
391 if 'error' in response
392 ]
393 if errors:
394 raise BuildbucketResponseException(
395 'Failed to schedule builds for some bots:\n%s' % '\n'.join(errors))
396
397
Quinten Yearsley777660f2020-03-04 23:37:06 +0000398def _make_tryjob_schedule_requests(changelist, jobs, options, patchset):
Quinten Yearsleyee8be8a2020-03-05 21:48:32 +0000399 """Constructs requests for Buildbucket to trigger tryjobs."""
Edward Lemurf0faf482019-09-25 20:40:17 +0000400 gerrit_changes = [changelist.GetGerritChange(patchset)]
Quinten Yearsleyee8be8a2020-03-05 21:48:32 +0000401 shared_properties = {
402 'category': options.ensure_value('category', 'git_cl_try')
403 }
404 if options.ensure_value('clobber', False):
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000405 shared_properties['clobber'] = True
406 shared_properties.update(_get_properties_from_options(options) or {})
407
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000408 shared_tags = [{'key': 'user_agent', 'value': 'git_cl_try'}]
Quinten Yearsleyee8be8a2020-03-05 21:48:32 +0000409 if options.ensure_value('retry_failed', False):
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000410 shared_tags.append({'key': 'retry_failed',
411 'value': '1'})
412
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000413 requests = []
Edward Lemur45768512020-03-02 19:03:14 +0000414 for (project, bucket, builder) in jobs:
415 properties = shared_properties.copy()
416 if 'presubmit' in builder.lower():
417 properties['dry_run'] = 'true'
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000418
Edward Lemur45768512020-03-02 19:03:14 +0000419 requests.append({
420 'scheduleBuild': {
421 'requestId': str(uuid.uuid4()),
422 'builder': {
423 'project': getattr(options, 'project', None) or project,
424 'bucket': bucket,
425 'builder': builder,
426 },
427 'gerritChanges': gerrit_changes,
428 'properties': properties,
429 'tags': [
430 {'key': 'builder', 'value': builder},
431 ] + shared_tags,
432 }
433 })
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000434
Quinten Yearsleyee8be8a2020-03-05 21:48:32 +0000435 if options.ensure_value('revision', None):
Josip Sokcevic9011a5b2021-02-12 18:59:44 +0000436 remote, remote_branch = changelist.GetRemoteBranch()
Edward Lemur45768512020-03-02 19:03:14 +0000437 requests[-1]['scheduleBuild']['gitilesCommit'] = {
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000438 'host': _canonical_git_googlesource_host(gerrit_changes[0]['host']),
Edward Lemur45768512020-03-02 19:03:14 +0000439 'project': gerrit_changes[0]['project'],
Josip Sokcevic9011a5b2021-02-12 18:59:44 +0000440 'id': options.revision,
441 'ref': GetTargetRef(remote, remote_branch, None)
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000442 }
Anthony Polito1a5fe232020-01-24 23:17:52 +0000443
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000444 return requests
kjellander@chromium.org44424542015-06-02 18:35:29 +0000445
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000446
Quinten Yearsley777660f2020-03-04 23:37:06 +0000447def _fetch_tryjobs(changelist, buildbucket_host, patchset=None):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000448 """Fetches tryjobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000449
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000450 Returns list of buildbucket.v2.Build with the try jobs for the changelist.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000451 """
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000452 fields = ['id', 'builder', 'status', 'createTime', 'tags']
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000453 request = {
454 'predicate': {
455 'gerritChanges': [changelist.GetGerritChange(patchset)],
456 },
457 'fields': ','.join('builds.*.' + field for field in fields),
458 }
tandrii221ab252016-10-06 08:12:04 -0700459
Edward Lemur5b929a42019-10-21 17:57:39 +0000460 authenticator = auth.Authenticator()
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000461 if authenticator.has_cached_credentials():
462 http = authenticator.authorize(httplib2.Http())
463 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700464 print('Warning: Some results might be missing because %s' %
465 # Get the message on how to login.
Andrii Shyshkalov2517afd2021-01-19 17:07:43 +0000466 (str(auth.LoginRequiredError()),))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000467 http = httplib2.Http()
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000468 http.force_exception_to_status_code = True
469
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000470 response = _call_buildbucket(http, buildbucket_host, 'SearchBuilds', request)
471 return response.get('builds', [])
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000472
Edward Lemur45768512020-03-02 19:03:14 +0000473
Edward Lemur5b929a42019-10-21 17:57:39 +0000474def _fetch_latest_builds(changelist, buildbucket_host, latest_patchset=None):
Quinten Yearsley983111f2019-09-26 17:18:48 +0000475 """Fetches builds from the latest patchset that has builds (within
476 the last few patchsets).
477
478 Args:
Quinten Yearsley983111f2019-09-26 17:18:48 +0000479 changelist (Changelist): The CL to fetch builds for
480 buildbucket_host (str): Buildbucket host, e.g. "cr-buildbucket.appspot.com"
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +0000481 lastest_patchset(int|NoneType): the patchset to start fetching builds from.
482 If None (default), starts with the latest available patchset.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000483 Returns:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000484 A tuple (builds, patchset) where builds is a list of buildbucket.v2.Build,
485 and patchset is the patchset number where those builds came from.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000486 """
487 assert buildbucket_host
488 assert changelist.GetIssue(), 'CL must be uploaded first'
489 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +0000490 if latest_patchset is None:
491 assert changelist.GetMostRecentPatchset()
492 ps = changelist.GetMostRecentPatchset()
493 else:
494 assert latest_patchset > 0, latest_patchset
495 ps = latest_patchset
496
Quinten Yearsley983111f2019-09-26 17:18:48 +0000497 min_ps = max(1, ps - 5)
498 while ps >= min_ps:
Quinten Yearsley777660f2020-03-04 23:37:06 +0000499 builds = _fetch_tryjobs(changelist, buildbucket_host, patchset=ps)
Quinten Yearsley983111f2019-09-26 17:18:48 +0000500 if len(builds):
501 return builds, ps
502 ps -= 1
503 return [], 0
504
505
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000506def _filter_failed_for_retry(all_builds):
507 """Returns a list of buckets/builders that are worth retrying.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000508
509 Args:
Quinten Yearsley777660f2020-03-04 23:37:06 +0000510 all_builds (list): Builds, in the format returned by _fetch_tryjobs,
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000511 i.e. a list of buildbucket.v2.Builds which includes status and builder
512 info.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000513
514 Returns:
Edward Lemur45768512020-03-02 19:03:14 +0000515 A dict {(proj, bucket): [builders]}. This is the same format accepted by
Quinten Yearsley777660f2020-03-04 23:37:06 +0000516 _trigger_tryjobs.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000517 """
Edward Lemur45768512020-03-02 19:03:14 +0000518 grouped = {}
519 for build in all_builds:
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000520 builder = build['builder']
Edward Lemur45768512020-03-02 19:03:14 +0000521 key = (builder['project'], builder['bucket'], builder['builder'])
522 grouped.setdefault(key, []).append(build)
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000523
Edward Lemur45768512020-03-02 19:03:14 +0000524 jobs = []
525 for (project, bucket, builder), builds in grouped.items():
526 if 'triggered' in builder:
527 print('WARNING: Not scheduling %s. Triggered bots require an initial job '
528 'from a parent. Please schedule a manual job for the parent '
529 'instead.')
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000530 continue
531 if any(b['status'] in ('STARTED', 'SCHEDULED') for b in builds):
532 # Don't retry if any are running.
533 continue
Edward Lemur45768512020-03-02 19:03:14 +0000534 # If builder had several builds, retry only if the last one failed.
535 # This is a bit different from CQ, which would re-use *any* SUCCESS-full
536 # build, but in case of retrying failed jobs retrying a flaky one makes
537 # sense.
538 builds = sorted(builds, key=lambda b: b['createTime'])
539 if builds[-1]['status'] not in ('FAILURE', 'INFRA_FAILURE'):
540 continue
541 # Don't retry experimental build previously triggered by CQ.
542 if any(t['key'] == 'cq_experimental' and t['value'] == 'true'
543 for t in builds[-1]['tags']):
544 continue
545 jobs.append((project, bucket, builder))
546
547 # Sort the jobs to make testing easier.
548 return sorted(jobs)
Quinten Yearsley983111f2019-09-26 17:18:48 +0000549
550
Quinten Yearsley777660f2020-03-04 23:37:06 +0000551def _print_tryjobs(options, builds):
552 """Prints nicely result of _fetch_tryjobs."""
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000553 if not builds:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000554 print('No tryjobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000555 return
556
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000557 longest_builder = max(len(b['builder']['builder']) for b in builds)
558 name_fmt = '{builder:<%d}' % longest_builder
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000559 if options.print_master:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000560 longest_bucket = max(len(b['builder']['bucket']) for b in builds)
561 name_fmt = ('{bucket:>%d} ' % longest_bucket) + name_fmt
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000562
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000563 builds_by_status = {}
564 for b in builds:
565 builds_by_status.setdefault(b['status'], []).append({
566 'id': b['id'],
567 'name': name_fmt.format(
568 builder=b['builder']['builder'], bucket=b['builder']['bucket']),
569 })
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000570
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000571 sort_key = lambda b: (b['name'], b['id'])
572
573 def print_builds(title, builds, fmt=None, color=None):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000574 """Pop matching builds from `builds` dict and print them."""
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000575 if not builds:
576 return
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000577
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000578 fmt = fmt or '{name} https://ci.chromium.org/b/{id}'
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000579 if not options.color or color is None:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000580 colorize = lambda x: x
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000581 else:
582 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
583
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000584 print(colorize(title))
585 for b in sorted(builds, key=sort_key):
586 print(' ', colorize(fmt.format(**b)))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000587
588 total = len(builds)
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000589 print_builds(
590 'Successes:', builds_by_status.pop('SUCCESS', []), color=Fore.GREEN)
591 print_builds(
592 'Infra Failures:', builds_by_status.pop('INFRA_FAILURE', []),
593 color=Fore.MAGENTA)
594 print_builds('Failures:', builds_by_status.pop('FAILURE', []), color=Fore.RED)
595 print_builds('Canceled:', builds_by_status.pop('CANCELED', []), fmt='{name}',
596 color=Fore.MAGENTA)
Andrii Shyshkalov792630c2020-10-19 16:47:44 +0000597 print_builds('Started:', builds_by_status.pop('STARTED', []),
598 color=Fore.YELLOW)
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000599 print_builds(
600 'Scheduled:', builds_by_status.pop('SCHEDULED', []), fmt='{name} id={id}')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000601 # The last section is just in case buildbucket API changes OR there is a bug.
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000602 print_builds(
603 'Other:', sum(builds_by_status.values(), []), fmt='{name} id={id}')
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000604 print('Total: %d tryjobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000605
606
Aiden Bennerc08566e2018-10-03 17:52:42 +0000607def _ComputeDiffLineRanges(files, upstream_commit):
608 """Gets the changed line ranges for each file since upstream_commit.
609
610 Parses a git diff on provided files and returns a dict that maps a file name
611 to an ordered list of range tuples in the form (start_line, count).
612 Ranges are in the same format as a git diff.
613 """
614 # If files is empty then diff_output will be a full diff.
615 if len(files) == 0:
616 return {}
617
Aiden Benner6c18a1a2018-11-23 20:18:23 +0000618 # Take the git diff and find the line ranges where there are changes.
Jamie Madill3671a6a2019-10-24 15:13:21 +0000619 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000620 diff_output = RunGit(diff_cmd)
621
622 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
623 # 2 capture groups
624 # 0 == fname of diff file
625 # 1 == 'diff_start,diff_count' or 'diff_start'
626 # will match each of
627 # diff --git a/foo.foo b/foo.py
628 # @@ -12,2 +14,3 @@
629 # @@ -12,2 +17 @@
630 # running re.findall on the above string with pattern will give
631 # [('foo.py', ''), ('', '14,3'), ('', '17')]
632
633 curr_file = None
634 line_diffs = {}
635 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
636 if match[0] != '':
637 # Will match the second filename in diff --git a/a.py b/b.py.
638 curr_file = match[0]
639 line_diffs[curr_file] = []
640 else:
641 # Matches +14,3
642 if ',' in match[1]:
643 diff_start, diff_count = match[1].split(',')
644 else:
645 # Single line changes are of the form +12 instead of +12,1.
646 diff_start = match[1]
647 diff_count = 1
648
649 diff_start = int(diff_start)
650 diff_count = int(diff_count)
651
652 # If diff_count == 0 this is a removal we can ignore.
653 line_diffs[curr_file].append((diff_start, diff_count))
654
655 return line_diffs
656
657
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000658def _FindYapfConfigFile(fpath, yapf_config_cache, top_dir=None):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000659 """Checks if a yapf file is in any parent directory of fpath until top_dir.
660
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000661 Recursively checks parent directories to find yapf file and if no yapf file
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000662 is found returns None. Uses yapf_config_cache as a cache for previously found
663 configs.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000664 """
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000665 fpath = os.path.abspath(fpath)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000666 # Return result if we've already computed it.
667 if fpath in yapf_config_cache:
668 return yapf_config_cache[fpath]
669
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000670 parent_dir = os.path.dirname(fpath)
671 if os.path.isfile(fpath):
672 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000673 else:
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000674 # Otherwise fpath is a directory
675 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
676 if os.path.isfile(yapf_file):
677 ret = yapf_file
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000678 elif fpath in (top_dir, parent_dir):
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000679 # If we're at the top level directory, or if we're at root
680 # there is no provided style.
681 ret = None
682 else:
683 # Otherwise recurse on the current directory.
684 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000685 yapf_config_cache[fpath] = ret
686 return ret
687
688
Brian Sheedyb4307d52019-12-02 19:18:17 +0000689def _GetYapfIgnorePatterns(top_dir):
690 """Returns all patterns in the .yapfignore file.
Brian Sheedy59b06a82019-10-14 17:03:29 +0000691
692 yapf is supposed to handle the ignoring of files listed in .yapfignore itself,
693 but this functionality appears to break when explicitly passing files to
694 yapf for formatting. According to
Josip Sokcevicc39ab992020-09-24 20:09:15 +0000695 https://github.com/google/yapf/blob/HEAD/README.rst#excluding-files-from-formatting-yapfignore,
Brian Sheedy59b06a82019-10-14 17:03:29 +0000696 the .yapfignore file should be in the directory that yapf is invoked from,
697 which we assume to be the top level directory in this case.
698
699 Args:
700 top_dir: The top level directory for the repository being formatted.
701
702 Returns:
Brian Sheedyb4307d52019-12-02 19:18:17 +0000703 A set of all fnmatch patterns to be ignored.
Brian Sheedy59b06a82019-10-14 17:03:29 +0000704 """
705 yapfignore_file = os.path.join(top_dir, '.yapfignore')
Brian Sheedyb4307d52019-12-02 19:18:17 +0000706 ignore_patterns = set()
Brian Sheedy59b06a82019-10-14 17:03:29 +0000707 if not os.path.exists(yapfignore_file):
Brian Sheedyb4307d52019-12-02 19:18:17 +0000708 return ignore_patterns
Brian Sheedy59b06a82019-10-14 17:03:29 +0000709
Anthony Politoc64e3902021-04-30 21:55:25 +0000710 for line in gclient_utils.FileRead(yapfignore_file).split('\n'):
711 stripped_line = line.strip()
712 # Comments and blank lines should be ignored.
713 if stripped_line.startswith('#') or stripped_line == '':
714 continue
715 ignore_patterns.add(stripped_line)
Brian Sheedyb4307d52019-12-02 19:18:17 +0000716 return ignore_patterns
717
718
719def _FilterYapfIgnoredFiles(filepaths, patterns):
720 """Filters out any filepaths that match any of the given patterns.
721
722 Args:
723 filepaths: An iterable of strings containing filepaths to filter.
724 patterns: An iterable of strings containing fnmatch patterns to filter on.
725
726 Returns:
727 A list of strings containing all the elements of |filepaths| that did not
728 match any of the patterns in |patterns|.
729 """
730 # Not inlined so that tests can use the same implementation.
731 return [f for f in filepaths
732 if not any(fnmatch.fnmatch(f, p) for p in patterns)]
Brian Sheedy59b06a82019-10-14 17:03:29 +0000733
734
Aaron Gable13101a62018-02-09 13:20:41 -0800735def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000736 """Prints statistics about the change to the user."""
737 # --no-ext-diff is broken in some versions of Git, so try to work around
738 # this by overriding the environment (but there is still a problem if the
739 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000740 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000741 if 'GIT_EXTERNAL_DIFF' in env:
742 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000743
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000744 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800745 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
Edward Lemur0db01f02019-11-12 22:01:51 +0000746 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000747
748
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000749class BuildbucketResponseException(Exception):
750 pass
751
752
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000753class Settings(object):
754 def __init__(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000755 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000756 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000757 self.tree_status_url = None
758 self.viewvc_url = None
759 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000760 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000761 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000762 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000763 self.git_editor = None
Jamie Madilldc4d19e2019-10-24 21:50:02 +0000764 self.format_full_by_default = None
Sigurd Schneider1bfda8e2021-06-30 14:46:25 +0000765 self.is_status_commit_order_by_date = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000766
Edward Lemur26964072020-02-19 19:18:51 +0000767 def _LazyUpdateIfNeeded(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000768 """Updates the settings from a codereview.settings file, if available."""
Edward Lemur26964072020-02-19 19:18:51 +0000769 if self.updated:
770 return
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000771
Edward Lemur26964072020-02-19 19:18:51 +0000772 # The only value that actually changes the behavior is
773 # autoupdate = "false". Everything else means "true".
774 autoupdate = (
775 scm.GIT.GetConfig(self.GetRoot(), 'rietveld.autoupdate', '').lower())
776
777 cr_settings_file = FindCodereviewSettingsFile()
778 if autoupdate != 'false' and cr_settings_file:
779 LoadCodereviewSettingsFromFile(cr_settings_file)
780 cr_settings_file.close()
781
782 self.updated = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000783
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000784 @staticmethod
785 def GetRelativeRoot():
Edward Lesmes50da7702020-03-30 19:23:43 +0000786 return scm.GIT.GetCheckoutRoot('.')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000787
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000788 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000789 if self.root is None:
790 self.root = os.path.abspath(self.GetRelativeRoot())
791 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000792
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000793 def GetTreeStatusUrl(self, error_ok=False):
794 if not self.tree_status_url:
Edward Lemur26964072020-02-19 19:18:51 +0000795 self.tree_status_url = self._GetConfig('rietveld.tree-status-url')
796 if self.tree_status_url is None and not error_ok:
797 DieWithError(
798 'You must configure your tree status URL by running '
799 '"git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000800 return self.tree_status_url
801
802 def GetViewVCUrl(self):
803 if not self.viewvc_url:
Edward Lemur26964072020-02-19 19:18:51 +0000804 self.viewvc_url = self._GetConfig('rietveld.viewvc-url')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000805 return self.viewvc_url
806
rmistry@google.com90752582014-01-14 21:04:50 +0000807 def GetBugPrefix(self):
Edward Lemur26964072020-02-19 19:18:51 +0000808 return self._GetConfig('rietveld.bug-prefix')
rmistry@google.com78948ed2015-07-08 23:09:57 +0000809
rmistry@google.com5626a922015-02-26 14:03:30 +0000810 def GetRunPostUploadHook(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000811 run_post_upload_hook = self._GetConfig(
Edward Lemur26964072020-02-19 19:18:51 +0000812 'rietveld.run-post-upload-hook')
rmistry@google.com5626a922015-02-26 14:03:30 +0000813 return run_post_upload_hook == "True"
814
Joanna Wangc8f23e22023-01-19 21:18:10 +0000815 def GetDefaultCCList(self):
816 return self._GetConfig('rietveld.cc')
817
Dirk Pranke6f0df682021-06-25 00:42:33 +0000818 def GetUsePython3(self):
819 return self._GetConfig('rietveld.use-python3')
820
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000821 def GetSquashGerritUploads(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000822 """Returns True if uploads to Gerrit should be squashed by default."""
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000823 if self.squash_gerrit_uploads is None:
Edward Lesmes4de54132020-05-05 19:41:33 +0000824 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
825 if self.squash_gerrit_uploads is None:
Edward Lemur26964072020-02-19 19:18:51 +0000826 # Default is squash now (http://crbug.com/611892#c23).
827 self.squash_gerrit_uploads = self._GetConfig(
828 'gerrit.squash-uploads').lower() != 'false'
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000829 return self.squash_gerrit_uploads
830
Edward Lesmes4de54132020-05-05 19:41:33 +0000831 def GetSquashGerritUploadsOverride(self):
832 """Return True or False if codereview.settings should be overridden.
833
834 Returns None if no override has been defined.
835 """
836 # See also http://crbug.com/611892#c23
837 result = self._GetConfig('gerrit.override-squash-uploads').lower()
838 if result == 'true':
839 return True
840 if result == 'false':
841 return False
842 return None
843
Aleksey Khoroshilov35ef5ad2022-06-03 18:29:25 +0000844 def GetIsGerrit(self):
845 """Return True if gerrit.host is set."""
846 if self.is_gerrit is None:
847 self.is_gerrit = bool(self._GetConfig('gerrit.host', False))
848 return self.is_gerrit
849
tandrii@chromium.org28253532016-04-14 13:46:56 +0000850 def GetGerritSkipEnsureAuthenticated(self):
851 """Return True if EnsureAuthenticated should not be done for Gerrit
852 uploads."""
853 if self.gerrit_skip_ensure_authenticated is None:
Edward Lemur26964072020-02-19 19:18:51 +0000854 self.gerrit_skip_ensure_authenticated = self._GetConfig(
855 'gerrit.skip-ensure-authenticated').lower() == 'true'
tandrii@chromium.org28253532016-04-14 13:46:56 +0000856 return self.gerrit_skip_ensure_authenticated
857
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000858 def GetGitEditor(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000859 """Returns the editor specified in the git config, or None if none is."""
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000860 if self.git_editor is None:
Raul Tambre5a525872019-02-12 19:08:08 +0000861 # Git requires single quotes for paths with spaces. We need to replace
862 # them with double quotes for Windows to treat such paths as a single
863 # path.
Edward Lemur26964072020-02-19 19:18:51 +0000864 self.git_editor = self._GetConfig('core.editor').replace('\'', '"')
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000865 return self.git_editor or None
866
thestig@chromium.org44202a22014-03-11 19:22:18 +0000867 def GetLintRegex(self):
Edward Lemur26964072020-02-19 19:18:51 +0000868 return self._GetConfig('rietveld.cpplint-regex', DEFAULT_LINT_REGEX)
thestig@chromium.org44202a22014-03-11 19:22:18 +0000869
870 def GetLintIgnoreRegex(self):
Edward Lemur26964072020-02-19 19:18:51 +0000871 return self._GetConfig(
872 'rietveld.cpplint-ignore-regex', DEFAULT_LINT_IGNORE_REGEX)
thestig@chromium.org44202a22014-03-11 19:22:18 +0000873
Jamie Madilldc4d19e2019-10-24 21:50:02 +0000874 def GetFormatFullByDefault(self):
875 if self.format_full_by_default is None:
Jamie Madillac6f6232021-07-07 20:54:08 +0000876 self._LazyUpdateIfNeeded()
Jamie Madilldc4d19e2019-10-24 21:50:02 +0000877 result = (
878 RunGit(['config', '--bool', 'rietveld.format-full-by-default'],
879 error_ok=True).strip())
880 self.format_full_by_default = (result == 'true')
881 return self.format_full_by_default
882
Sigurd Schneider1bfda8e2021-06-30 14:46:25 +0000883 def IsStatusCommitOrderByDate(self):
884 if self.is_status_commit_order_by_date is None:
885 result = (RunGit(['config', '--bool', 'cl.date-order'],
886 error_ok=True).strip())
887 self.is_status_commit_order_by_date = (result == 'true')
888 return self.is_status_commit_order_by_date
889
Edward Lemur26964072020-02-19 19:18:51 +0000890 def _GetConfig(self, key, default=''):
891 self._LazyUpdateIfNeeded()
892 return scm.GIT.GetConfig(self.GetRoot(), key, default)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000893
894
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000895class _CQState(object):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +0000896 """Enum for states of CL with respect to CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000897 NONE = 'none'
Greg Gutermanbe5fccd2021-06-14 17:58:20 +0000898 QUICK_RUN = 'quick_run'
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000899 DRY_RUN = 'dry_run'
900 COMMIT = 'commit'
901
Greg Gutermanbe5fccd2021-06-14 17:58:20 +0000902 ALL_STATES = [NONE, QUICK_RUN, DRY_RUN, COMMIT]
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000903
904
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000905class _ParsedIssueNumberArgument(object):
Edward Lemurf38bc172019-09-03 21:02:13 +0000906 def __init__(self, issue=None, patchset=None, hostname=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000907 self.issue = issue
908 self.patchset = patchset
909 self.hostname = hostname
910
911 @property
912 def valid(self):
913 return self.issue is not None
914
915
Edward Lemurf38bc172019-09-03 21:02:13 +0000916def ParseIssueNumberArgument(arg):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000917 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
918 fail_result = _ParsedIssueNumberArgument()
919
Edward Lemur678a6842019-10-03 22:25:05 +0000920 if isinstance(arg, int):
921 return _ParsedIssueNumberArgument(issue=arg)
922 if not isinstance(arg, basestring):
923 return fail_result
924
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000925 if arg.isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +0000926 return _ParsedIssueNumberArgument(issue=int(arg))
Aaron Gableaee6c852017-06-26 12:49:01 -0700927
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000928 url = gclient_utils.UpgradeToHttps(arg)
Alex Turner30ae6372022-01-04 02:32:52 +0000929 if not url.startswith('http'):
930 return fail_result
Andrii Shyshkalov8aebb602020-04-16 22:10:27 +0000931 for gerrit_url, short_url in _KNOWN_GERRIT_TO_SHORT_URLS.items():
932 if url.startswith(short_url):
933 url = gerrit_url + url[len(short_url):]
934 break
935
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000936 try:
Edward Lemur79d4f992019-11-11 23:49:02 +0000937 parsed_url = urllib.parse.urlparse(url)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000938 except ValueError:
939 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +0200940
Alex Turner30ae6372022-01-04 02:32:52 +0000941 # If "https://" was automatically added, fail if `arg` looks unlikely to be a
942 # URL.
943 if not arg.startswith('http') and '.' not in parsed_url.netloc:
944 return fail_result
945
Edward Lemur678a6842019-10-03 22:25:05 +0000946 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
947 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
948 # Short urls like https://domain/<issue_number> can be used, but don't allow
949 # specifying the patchset (you'd 404), but we allow that here.
950 if parsed_url.path == '/':
951 part = parsed_url.fragment
952 else:
953 part = parsed_url.path
954
955 match = re.match(
956 r'(/c(/.*/\+)?)?/(?P<issue>\d+)(/(?P<patchset>\d+)?/?)?$', part)
957 if not match:
958 return fail_result
959
960 issue = int(match.group('issue'))
961 patchset = match.group('patchset')
962 return _ParsedIssueNumberArgument(
963 issue=issue,
964 patchset=int(patchset) if patchset else None,
965 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000966
967
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +0000968def _create_description_from_log(args):
969 """Pulls out the commit log to use as a base for the CL description."""
970 log_args = []
Bruce Dawson13acea32022-05-03 22:13:08 +0000971 if len(args) == 1 and args[0] == None:
972 # Handle the case where None is passed as the branch.
973 return ''
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +0000974 if len(args) == 1 and not args[0].endswith('.'):
975 log_args = [args[0] + '..']
976 elif len(args) == 1 and args[0].endswith('...'):
977 log_args = [args[0][:-1]]
978 elif len(args) == 2:
979 log_args = [args[0] + '..' + args[1]]
980 else:
981 log_args = args[:] # Hope for the best!
Manh Nguyene3644862020-08-05 18:25:46 +0000982 return RunGit(['log', '--pretty=format:%B%n'] + log_args)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +0000983
984
Aaron Gablea45ee112016-11-22 15:14:38 -0800985class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -0700986 def __init__(self, issue, url):
987 self.issue = issue
988 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -0800989 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -0700990
991 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -0800992 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -0700993 self.issue, self.url)
994
995
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +0100996_CommentSummary = collections.namedtuple(
Quinten Yearsley0e617c02019-02-20 00:37:03 +0000997 '_CommentSummary', ['date', 'message', 'sender', 'autogenerated',
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +0100998 # TODO(tandrii): these two aren't known in Gerrit.
999 'approval', 'disapproval'])
1000
1001
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001002class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001003 """Changelist works with one changelist in local branch.
1004
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001005 Notes:
1006 * Not safe for concurrent multi-{thread,process} use.
1007 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001008 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001009 """
1010
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00001011 def __init__(self,
1012 branchref=None,
1013 issue=None,
1014 codereview_host=None,
1015 commit_date=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001016 """Create a new ChangeList instance.
1017
Edward Lemurf38bc172019-09-03 21:02:13 +00001018 **kwargs will be passed directly to Gerrit implementation.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001019 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001020 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001021 global settings
1022 if not settings:
1023 # Happens when git_cl.py is used as a utility library.
1024 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001025
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001026 self.branchref = branchref
1027 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001028 assert branchref.startswith('refs/heads/')
Edward Lemur85153282020-02-14 22:06:29 +00001029 self.branch = scm.GIT.ShortBranchName(self.branchref)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001030 else:
1031 self.branch = None
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00001032 self.commit_date = commit_date
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001033 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001034 self.lookedup_issue = False
1035 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001036 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001037 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001038 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001039 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001040 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001041 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001042 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001043
Edward Lemur125d60a2019-09-13 18:25:41 +00001044 # Lazily cached values.
1045 self._gerrit_host = None # e.g. chromium-review.googlesource.com
1046 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Edward Lesmese1576912021-02-16 21:53:34 +00001047 self._owners_client = None
Edward Lemur125d60a2019-09-13 18:25:41 +00001048 # Map from change number (issue) to its detail cache.
1049 self._detail_cache = {}
1050
1051 if codereview_host is not None:
1052 assert not codereview_host.startswith('https://'), codereview_host
1053 self._gerrit_host = codereview_host
1054 self._gerrit_server = 'https://%s' % codereview_host
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001055
Edward Lesmese1576912021-02-16 21:53:34 +00001056 @property
1057 def owners_client(self):
1058 if self._owners_client is None:
1059 remote, remote_branch = self.GetRemoteBranch()
1060 branch = GetTargetRef(remote, remote_branch, None)
1061 self._owners_client = owners_client.GetCodeOwnersClient(
Edward Lesmese1576912021-02-16 21:53:34 +00001062 host=self.GetGerritHost(),
1063 project=self.GetGerritProject(),
1064 branch=branch)
1065 return self._owners_client
1066
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001067 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001068 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001069
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001070 The return value is a string suitable for passing to git cl with the --cc
1071 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001072 """
1073 if self.cc is None:
Joanna Wangc8f23e22023-01-19 21:18:10 +00001074 base_cc = settings.GetDefaultCCList()
1075 more_cc = ','.join(self.more_cc)
1076 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001077 return self.cc
1078
Daniel Cheng7227d212017-11-17 08:12:37 -08001079 def ExtendCC(self, more_cc):
1080 """Extends the list of users to cc on this CL based on the changed files."""
1081 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001082
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00001083 def GetCommitDate(self):
1084 """Returns the commit date as provided in the constructor"""
1085 return self.commit_date
1086
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001087 def GetBranch(self):
Josip Sokcevicc39ab992020-09-24 20:09:15 +00001088 """Returns the short branch name, e.g. 'main'."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001089 if not self.branch:
Edward Lemur85153282020-02-14 22:06:29 +00001090 branchref = scm.GIT.GetBranchRef(settings.GetRoot())
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001091 if not branchref:
1092 return None
1093 self.branchref = branchref
Edward Lemur85153282020-02-14 22:06:29 +00001094 self.branch = scm.GIT.ShortBranchName(self.branchref)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001095 return self.branch
1096
1097 def GetBranchRef(self):
Josip Sokcevicc39ab992020-09-24 20:09:15 +00001098 """Returns the full branch name, e.g. 'refs/heads/main'."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001099 self.GetBranch() # Poke the lazy loader.
1100 return self.branchref
1101
Edward Lemur85153282020-02-14 22:06:29 +00001102 def _GitGetBranchConfigValue(self, key, default=None):
1103 return scm.GIT.GetBranchConfig(
1104 settings.GetRoot(), self.GetBranch(), key, default)
tandrii5d48c322016-08-18 16:19:37 -07001105
Edward Lemur85153282020-02-14 22:06:29 +00001106 def _GitSetBranchConfigValue(self, key, value):
1107 action = 'set %s to %r' % (key, value)
1108 if not value:
1109 action = 'unset %s' % key
1110 assert self.GetBranch(), 'a branch is needed to ' + action
1111 return scm.GIT.SetBranchConfig(
1112 settings.GetRoot(), self.GetBranch(), key, value)
tandrii5d48c322016-08-18 16:19:37 -07001113
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001114 @staticmethod
1115 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001116 """Returns a tuple containing remote and remote ref,
Josip Sokcevicc39ab992020-09-24 20:09:15 +00001117 e.g. 'origin', 'refs/heads/main'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001118 """
Edward Lemur15a9b8c2020-02-13 00:52:30 +00001119 remote, upstream_branch = scm.GIT.FetchUpstreamTuple(
1120 settings.GetRoot(), branch)
1121 if not remote or not upstream_branch:
1122 DieWithError(
1123 'Unable to determine default branch to diff against.\n'
Josip Sokcevicb038f722021-01-06 18:28:11 +00001124 'Verify this branch is set up to track another \n'
1125 '(via the --track argument to "git checkout -b ..."). \n'
1126 'or pass complete "git diff"-style arguments if supported, like\n'
1127 ' git cl upload origin/main\n')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001128
1129 return remote, upstream_branch
1130
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001131 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001132 upstream_branch = self.GetUpstreamBranch()
Edward Lesmes50da7702020-03-30 19:23:43 +00001133 if not scm.GIT.IsValidRevision(settings.GetRoot(), upstream_branch):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001134 DieWithError('The upstream for the current branch (%s) does not exist '
1135 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001136 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001137 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001138
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001139 def GetUpstreamBranch(self):
1140 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001141 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Raul Tambrefe1dbe12019-05-02 04:43:57 +00001142 if remote != '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001143 upstream_branch = upstream_branch.replace('refs/heads/',
1144 'refs/remotes/%s/' % remote)
1145 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1146 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001147 self.upstream_branch = upstream_branch
1148 return self.upstream_branch
1149
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001150 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001151 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001152 remote, branch = None, self.GetBranch()
1153 seen_branches = set()
1154 while branch not in seen_branches:
1155 seen_branches.add(branch)
1156 remote, branch = self.FetchUpstreamTuple(branch)
Edward Lemur85153282020-02-14 22:06:29 +00001157 branch = scm.GIT.ShortBranchName(branch)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001158 if remote != '.' or branch.startswith('refs/remotes'):
1159 break
1160 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001161 remotes = RunGit(['remote'], error_ok=True).split()
1162 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001163 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001164 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001165 remote = 'origin'
Gavin Make6a62332020-12-04 21:57:10 +00001166 logging.warning('Could not determine which remote this change is '
1167 'associated with, so defaulting to "%s".' %
1168 self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001169 else:
Gavin Make6a62332020-12-04 21:57:10 +00001170 logging.warning('Could not determine which remote this change is '
1171 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001172 branch = 'HEAD'
1173 if branch.startswith('refs/remotes'):
1174 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001175 elif branch.startswith('refs/branch-heads/'):
1176 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001177 else:
1178 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001179 return self._remote
1180
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001181 def GetRemoteUrl(self):
1182 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1183
1184 Returns None if there is no remote.
1185 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001186 is_cached, value = self._cached_remote_url
1187 if is_cached:
1188 return value
1189
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001190 remote, _ = self.GetRemoteBranch()
Edward Lemur26964072020-02-19 19:18:51 +00001191 url = scm.GIT.GetConfig(settings.GetRoot(), 'remote.%s.url' % remote, '')
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001192
Edward Lemur298f2cf2019-02-22 21:40:39 +00001193 # Check if the remote url can be parsed as an URL.
Edward Lemur79d4f992019-11-11 23:49:02 +00001194 host = urllib.parse.urlparse(url).netloc
Edward Lemur298f2cf2019-02-22 21:40:39 +00001195 if host:
1196 self._cached_remote_url = (True, url)
1197 return url
1198
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001199 # If it cannot be parsed as an url, assume it is a local directory,
1200 # probably a git cache.
Edward Lemur298f2cf2019-02-22 21:40:39 +00001201 logging.warning('"%s" doesn\'t appear to point to a git host. '
1202 'Interpreting it as a local directory.', url)
1203 if not os.path.isdir(url):
1204 logging.error(
Josip906bfde2020-01-31 22:38:49 +00001205 'Remote "%(remote)s" for branch "%(branch)s" points to "%(url)s", '
1206 'but it doesn\'t exist.',
1207 {'remote': remote, 'branch': self.GetBranch(), 'url': url})
Edward Lemur298f2cf2019-02-22 21:40:39 +00001208 return None
1209
1210 cache_path = url
Edward Lemur26964072020-02-19 19:18:51 +00001211 url = scm.GIT.GetConfig(url, 'remote.%s.url' % remote, '')
Edward Lemur298f2cf2019-02-22 21:40:39 +00001212
Edward Lemur79d4f992019-11-11 23:49:02 +00001213 host = urllib.parse.urlparse(url).netloc
Edward Lemur298f2cf2019-02-22 21:40:39 +00001214 if not host:
1215 logging.error(
1216 'Remote "%(remote)s" for branch "%(branch)s" points to '
1217 '"%(cache_path)s", but it is misconfigured.\n'
1218 '"%(cache_path)s" must be a git repo and must have a remote named '
1219 '"%(remote)s" pointing to the git host.', {
1220 'remote': remote,
1221 'cache_path': cache_path,
1222 'branch': self.GetBranch()})
1223 return None
1224
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001225 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001226 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001227
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001228 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001229 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001230 if self.issue is None and not self.lookedup_issue:
Bruce Dawson13acea32022-05-03 22:13:08 +00001231 if self.GetBranch():
1232 self.issue = self._GitGetBranchConfigValue(ISSUE_CONFIG_KEY)
Edward Lemur85153282020-02-14 22:06:29 +00001233 if self.issue is not None:
1234 self.issue = int(self.issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001235 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001236 return self.issue
1237
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +00001238 def GetIssueURL(self, short=False):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001239 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001240 issue = self.GetIssue()
1241 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001242 return None
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +00001243 server = self.GetCodereviewServer()
1244 if short:
1245 server = _KNOWN_GERRIT_TO_SHORT_URLS.get(server, server)
1246 return '%s/%s' % (server, issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001247
Dirk Pranke6f0df682021-06-25 00:42:33 +00001248 def GetUsePython3(self):
Josip Sokcevic340edc32021-07-08 17:01:46 +00001249 return settings.GetUsePython3()
Dirk Pranke6f0df682021-06-25 00:42:33 +00001250
Edward Lemur6c6827c2020-02-06 21:15:18 +00001251 def FetchDescription(self, pretty=False):
1252 assert self.GetIssue(), 'issue is required to query Gerrit'
1253
Edward Lemur9aa1a962020-02-25 00:58:38 +00001254 if self.description is None:
Edward Lemur6c6827c2020-02-06 21:15:18 +00001255 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
1256 current_rev = data['current_revision']
1257 self.description = data['revisions'][current_rev]['commit']['message']
Edward Lemur6c6827c2020-02-06 21:15:18 +00001258
1259 if not pretty:
1260 return self.description
1261
1262 # Set width to 72 columns + 2 space indent.
1263 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
1264 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1265 lines = self.description.splitlines()
1266 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001267
1268 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001269 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001270 if self.patchset is None and not self.lookedup_patchset:
Bruce Dawson13acea32022-05-03 22:13:08 +00001271 if self.GetBranch():
1272 self.patchset = self._GitGetBranchConfigValue(PATCHSET_CONFIG_KEY)
Edward Lemur85153282020-02-14 22:06:29 +00001273 if self.patchset is not None:
1274 self.patchset = int(self.patchset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001275 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001276 return self.patchset
1277
Edward Lemur9aa1a962020-02-25 00:58:38 +00001278 def GetAuthor(self):
1279 return scm.GIT.GetConfig(settings.GetRoot(), 'user.email')
1280
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001281 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001282 """Set this branch's patchset. If patchset=0, clears the patchset."""
1283 assert self.GetBranch()
1284 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001285 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001286 else:
1287 self.patchset = int(patchset)
Edward Lesmes50da7702020-03-30 19:23:43 +00001288 self._GitSetBranchConfigValue(PATCHSET_CONFIG_KEY, str(self.patchset))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001289
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001290 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001291 """Set this branch's issue. If issue isn't given, clears the issue."""
1292 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001293 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001294 issue = int(issue)
Edward Lesmes50da7702020-03-30 19:23:43 +00001295 self._GitSetBranchConfigValue(ISSUE_CONFIG_KEY, str(issue))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001296 self.issue = issue
Edward Lemur125d60a2019-09-13 18:25:41 +00001297 codereview_server = self.GetCodereviewServer()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001298 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001299 self._GitSetBranchConfigValue(
Edward Lesmes50da7702020-03-30 19:23:43 +00001300 CODEREVIEW_SERVER_CONFIG_KEY, codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001301 else:
tandrii5d48c322016-08-18 16:19:37 -07001302 # Reset all of these just to be clean.
1303 reset_suffixes = [
Gavin Makbe2e9262022-11-08 23:41:55 +00001304 LAST_UPLOAD_HASH_CONFIG_KEY,
Edward Lesmes50da7702020-03-30 19:23:43 +00001305 ISSUE_CONFIG_KEY,
1306 PATCHSET_CONFIG_KEY,
1307 CODEREVIEW_SERVER_CONFIG_KEY,
Gavin Makbe2e9262022-11-08 23:41:55 +00001308 GERRIT_SQUASH_HASH_CONFIG_KEY,
Edward Lesmes50da7702020-03-30 19:23:43 +00001309 ]
tandrii5d48c322016-08-18 16:19:37 -07001310 for prop in reset_suffixes:
Edward Lemur85153282020-02-14 22:06:29 +00001311 try:
1312 self._GitSetBranchConfigValue(prop, None)
1313 except subprocess2.CalledProcessError:
1314 pass
Aaron Gableca01e2c2017-07-19 11:16:02 -07001315 msg = RunGit(['log', '-1', '--format=%B']).strip()
1316 if msg and git_footers.get_footer_change_id(msg):
1317 print('WARNING: The change patched into this branch has a Change-Id. '
1318 'Removing it.')
1319 RunGit(['commit', '--amend', '-m',
1320 git_footers.remove_footer(msg, 'Change-Id')])
Edward Lemurf38bc172019-09-03 21:02:13 +00001321 self.lookedup_issue = True
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001322 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001323 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001324
Edward Lemur2c62b332020-03-12 22:12:33 +00001325 def GetAffectedFiles(self, upstream):
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001326 try:
Edward Lemur2c62b332020-03-12 22:12:33 +00001327 return [f for _, f in scm.GIT.CaptureStatus(settings.GetRoot(), upstream)]
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001328 except subprocess2.CalledProcessError:
1329 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001330 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001331 'This branch probably doesn\'t exist anymore. To reset the\n'
1332 'tracking branch, please run\n'
Josip Sokcevicc39ab992020-09-24 20:09:15 +00001333 ' git branch --set-upstream-to origin/main %s\n'
1334 'or replace origin/main with the relevant branch') %
Edward Lemur2c62b332020-03-12 22:12:33 +00001335 (upstream, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001336
dsansomee2d6fd92016-09-08 00:10:47 -07001337 def UpdateDescription(self, description, force=False):
Edward Lemur6c6827c2020-02-06 21:15:18 +00001338 assert self.GetIssue(), 'issue is required to update description'
1339
1340 if gerrit_util.HasPendingChangeEdit(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001341 self.GetGerritHost(), self._GerritChangeIdentifier()):
Edward Lemur6c6827c2020-02-06 21:15:18 +00001342 if not force:
1343 confirm_or_exit(
1344 'The description cannot be modified while the issue has a pending '
1345 'unpublished edit. Either publish the edit in the Gerrit web UI '
1346 'or delete it.\n\n', action='delete the unpublished edit')
1347
1348 gerrit_util.DeletePendingChangeEdit(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001349 self.GetGerritHost(), self._GerritChangeIdentifier())
Edward Lemur6c6827c2020-02-06 21:15:18 +00001350 gerrit_util.SetCommitMessage(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001351 self.GetGerritHost(), self._GerritChangeIdentifier(),
Edward Lemur6c6827c2020-02-06 21:15:18 +00001352 description, notify='NONE')
1353
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001354 self.description = description
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001355
Edward Lemur75526302020-02-27 22:31:05 +00001356 def _GetCommonPresubmitArgs(self, verbose, upstream):
Edward Lemur227d5102020-02-25 23:45:35 +00001357 args = [
Edward Lemur227d5102020-02-25 23:45:35 +00001358 '--root', settings.GetRoot(),
1359 '--upstream', upstream,
1360 ]
1361
1362 args.extend(['--verbose'] * verbose)
1363
Edward Lesmeseb1bd622021-03-01 19:54:07 +00001364 remote, remote_branch = self.GetRemoteBranch()
1365 target_ref = GetTargetRef(remote, remote_branch, None)
Aleksey Khoroshilov35ef5ad2022-06-03 18:29:25 +00001366 if settings.GetIsGerrit():
1367 args.extend(['--gerrit_url', self.GetCodereviewServer()])
1368 args.extend(['--gerrit_project', self.GetGerritProject()])
1369 args.extend(['--gerrit_branch', target_ref])
Edward Lesmeseb1bd622021-03-01 19:54:07 +00001370
Edward Lemur99df04e2020-03-05 19:39:43 +00001371 author = self.GetAuthor()
Edward Lemur227d5102020-02-25 23:45:35 +00001372 issue = self.GetIssue()
1373 patchset = self.GetPatchset()
Edward Lemur99df04e2020-03-05 19:39:43 +00001374 if author:
1375 args.extend(['--author', author])
Edward Lemur227d5102020-02-25 23:45:35 +00001376 if issue:
1377 args.extend(['--issue', str(issue)])
1378 if patchset:
1379 args.extend(['--patchset', str(patchset)])
Edward Lemur227d5102020-02-25 23:45:35 +00001380
Edward Lemur75526302020-02-27 22:31:05 +00001381 return args
1382
Josip Sokcevic017544d2022-03-31 23:47:53 +00001383 def RunHook(self,
1384 committing,
1385 may_prompt,
1386 verbose,
1387 parallel,
1388 upstream,
1389 description,
1390 all_files,
1391 files=None,
1392 resultdb=False,
1393 realm=None):
Edward Lemur75526302020-02-27 22:31:05 +00001394 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1395 args = self._GetCommonPresubmitArgs(verbose, upstream)
1396 args.append('--commit' if committing else '--upload')
Edward Lemur227d5102020-02-25 23:45:35 +00001397 if may_prompt:
1398 args.append('--may_prompt')
1399 if parallel:
1400 args.append('--parallel')
1401 if all_files:
1402 args.append('--all_files')
Josip Sokcevic017544d2022-03-31 23:47:53 +00001403 if files:
1404 args.extend(files.split(';'))
1405 args.append('--source_controlled_only')
Bruce Dawson09c0c072022-05-26 20:28:58 +00001406 if files or all_files:
1407 args.append('--no_diffs')
Edward Lemur227d5102020-02-25 23:45:35 +00001408
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001409 if resultdb and not realm:
1410 # TODO (crbug.com/1113463): store realm somewhere and look it up so
1411 # it is not required to pass the realm flag
1412 print('Note: ResultDB reporting will NOT be performed because --realm'
1413 ' was not specified. To enable ResultDB, please run the command'
1414 ' again with the --realm argument to specify the LUCI realm.')
1415
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001416 py3_results = self._RunPresubmit(args, resultdb, realm, description,
1417 use_python3=True)
Josip Sokcevic632bbc02022-05-19 05:32:50 +00001418 if py3_results.get('skipped_presubmits', 1) == 0:
1419 print('No more presubmits to run - skipping Python 2 presubmits.')
1420 return py3_results
1421
1422 py2_results = self._RunPresubmit(args, resultdb, realm, description,
1423 use_python3=False)
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001424 return self._MergePresubmitResults(py2_results, py3_results)
1425
1426 def _RunPresubmit(self, args, resultdb, realm, description, use_python3):
1427 args = args[:]
1428 vpython = 'vpython3' if use_python3 else 'vpython'
1429
Edward Lemur227d5102020-02-25 23:45:35 +00001430 with gclient_utils.temporary_file() as description_file:
1431 with gclient_utils.temporary_file() as json_output:
Edward Lemur1a83da12020-03-04 21:18:36 +00001432 gclient_utils.FileWrite(description_file, description)
Edward Lemur227d5102020-02-25 23:45:35 +00001433 args.extend(['--json_output', json_output])
1434 args.extend(['--description_file', description_file])
Dirk Pranke6f0df682021-06-25 00:42:33 +00001435 if self.GetUsePython3():
1436 args.append('--use-python3')
Edward Lemur227d5102020-02-25 23:45:35 +00001437 start = time_time()
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001438 cmd = [vpython, PRESUBMIT_SUPPORT] + args
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001439 if resultdb and realm:
1440 cmd = ['rdb', 'stream', '-new', '-realm', realm, '--'] + cmd
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001441
1442 p = subprocess2.Popen(cmd)
Edward Lemur227d5102020-02-25 23:45:35 +00001443 exit_code = p.wait()
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001444
Edward Lemur227d5102020-02-25 23:45:35 +00001445 metrics.collector.add_repeated('sub_commands', {
1446 'command': 'presubmit',
1447 'execution_time': time_time() - start,
1448 'exit_code': exit_code,
1449 })
1450
1451 if exit_code:
1452 sys.exit(exit_code)
1453
1454 json_results = gclient_utils.FileRead(json_output)
1455 return json.loads(json_results)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001456
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001457 def _MergePresubmitResults(self, py2_results, py3_results):
1458 return {
1459 'more_cc': sorted(set(py2_results.get('more_cc', []) +
1460 py3_results.get('more_cc', []))),
1461 'errors': (
1462 py2_results.get('errors', []) + py3_results.get('errors', [])),
1463 'notifications': (
1464 py2_results.get('notifications', []) +
1465 py3_results.get('notifications', [])),
1466 'warnings': (
1467 py2_results.get('warnings', []) + py3_results.get('warnings', []))
1468 }
1469
Brian Sheedy7326ca22022-11-02 18:36:17 +00001470 def RunPostUploadHook(self, verbose, upstream, description, py3_only):
Edward Lemur75526302020-02-27 22:31:05 +00001471 args = self._GetCommonPresubmitArgs(verbose, upstream)
1472 args.append('--post_upload')
1473
1474 with gclient_utils.temporary_file() as description_file:
Edward Lemur1a83da12020-03-04 21:18:36 +00001475 gclient_utils.FileWrite(description_file, description)
Edward Lemur75526302020-02-27 22:31:05 +00001476 args.extend(['--description_file', description_file])
Brian Sheedy7326ca22022-11-02 18:36:17 +00001477 if not py3_only:
1478 p_py2 = subprocess2.Popen(['vpython', PRESUBMIT_SUPPORT] + args)
Josip Sokcevice293d3d2022-02-16 22:52:15 +00001479 p_py3 = subprocess2.Popen(['vpython3', PRESUBMIT_SUPPORT] + args +
1480 ['--use-python3'])
Brian Sheedy7326ca22022-11-02 18:36:17 +00001481 if not py3_only:
1482 p_py2.wait()
Josip Sokcevice293d3d2022-02-16 22:52:15 +00001483 p_py3.wait()
Edward Lemur75526302020-02-27 22:31:05 +00001484
Edward Lemur5a644f82020-03-18 16:44:57 +00001485 def _GetDescriptionForUpload(self, options, git_diff_args, files):
1486 # Get description message for upload.
1487 if self.GetIssue():
1488 description = self.FetchDescription()
1489 elif options.message:
1490 description = options.message
1491 else:
1492 description = _create_description_from_log(git_diff_args)
1493 if options.title and options.squash:
Edward Lesmes0dd54822020-03-26 18:24:25 +00001494 description = options.title + '\n\n' + description
Edward Lemur5a644f82020-03-18 16:44:57 +00001495
Edward Lemur5a644f82020-03-18 16:44:57 +00001496 bug = options.bug
1497 fixed = options.fixed
Josip Sokcevic340edc32021-07-08 17:01:46 +00001498 if not self.GetIssue():
1499 # Extract bug number from branch name, but only if issue is being created.
1500 # It must start with bug or fix, followed by _ or - and number.
1501 # Optionally, it may contain _ or - after number with arbitrary text.
1502 # Examples:
1503 # bug-123
1504 # bug_123
1505 # fix-123
1506 # fix-123-some-description
1507 match = re.match(
1508 r'^(?P<type>bug|fix(?:e[sd])?)[_-]?(?P<bugnum>\d+)([-_]|$)',
1509 self.GetBranch())
1510 if not bug and not fixed and match:
1511 if match.group('type') == 'bug':
1512 bug = match.group('bugnum')
1513 else:
1514 fixed = match.group('bugnum')
Edward Lemur5a644f82020-03-18 16:44:57 +00001515
1516 change_description = ChangeDescription(description, bug, fixed)
1517
Joanna Wang39811b12023-01-20 23:09:48 +00001518 # Fill gaps in OWNERS coverage to reviewers if requested.
Edward Lesmes8c43c3f2021-01-20 00:20:26 +00001519 if options.add_owners_to:
Joanna Wang39811b12023-01-20 23:09:48 +00001520 assert options.add_owners_to in ('R'), options.add_owners_to
Edward Lesmese1576912021-02-16 21:53:34 +00001521 status = self.owners_client.GetFilesApprovalStatus(
Joanna Wang39811b12023-01-20 23:09:48 +00001522 files, [], options.reviewers)
Edward Lesmes8c43c3f2021-01-20 00:20:26 +00001523 missing_files = [
1524 f for f in files
Edward Lesmese1576912021-02-16 21:53:34 +00001525 if status[f] == self._owners_client.INSUFFICIENT_REVIEWERS
Edward Lesmes8c43c3f2021-01-20 00:20:26 +00001526 ]
Edward Lesmese1576912021-02-16 21:53:34 +00001527 owners = self.owners_client.SuggestOwners(
1528 missing_files, exclude=[self.GetAuthor()])
Joanna Wang39811b12023-01-20 23:09:48 +00001529 assert isinstance(options.reviewers, list), options.reviewers
1530 options.reviewers.extend(owners)
Edward Lesmes8c43c3f2021-01-20 00:20:26 +00001531
Edward Lemur5a644f82020-03-18 16:44:57 +00001532 # Set the reviewer list now so that presubmit checks can access it.
Joanna Wang39811b12023-01-20 23:09:48 +00001533 if options.reviewers:
1534 change_description.update_reviewers(options.reviewers)
Edward Lemur5a644f82020-03-18 16:44:57 +00001535
1536 return change_description
1537
1538 def _GetTitleForUpload(self, options):
manukh566e9d02022-06-30 19:49:53 +00001539 # When not squashing or options.title is provided, just return
1540 # options.title.
1541 if not options.squash or options.title:
Edward Lemur5a644f82020-03-18 16:44:57 +00001542 return options.title
1543
1544 # On first upload, patchset title is always this string, while options.title
1545 # gets converted to first line of message.
1546 if not self.GetIssue():
1547 return 'Initial upload'
1548
1549 # When uploading subsequent patchsets, options.message is taken as the title
1550 # if options.title is not provided.
Edward Lemur5a644f82020-03-18 16:44:57 +00001551 if options.message:
1552 return options.message.strip()
1553
1554 # Use the subject of the last commit as title by default.
Edward Lesmes50da7702020-03-30 19:23:43 +00001555 title = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
Mario Bianuccicebfb4e2020-07-22 23:08:16 +00001556 if options.force or options.skip_title:
Edward Lemur5a644f82020-03-18 16:44:57 +00001557 return title
Edward Lesmesae3586b2020-03-23 21:21:14 +00001558 user_title = gclient_utils.AskForData('Title for patchset [%s]: ' % title)
mlcui3da91712021-05-05 10:00:30 +00001559
1560 # Use the default title if the user confirms the default with a 'y'.
1561 if user_title.lower() == 'y':
1562 return title
Edward Lesmesae3586b2020-03-23 21:21:14 +00001563 return user_title or title
Edward Lemur5a644f82020-03-18 16:44:57 +00001564
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001565 def CMDUpload(self, options, git_diff_args, orig_args):
1566 """Uploads a change to codereview."""
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001567 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001568 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001569 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001570 else:
1571 if self.GetBranch() is None:
1572 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1573
1574 # Default to diffing against common ancestor of upstream branch
1575 base_branch = self.GetCommonAncestorWithUpstream()
1576 git_diff_args = [base_branch, 'HEAD']
1577
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001578 # Fast best-effort checks to abort before running potentially expensive
1579 # hooks if uploading is likely to fail anyway. Passing these checks does
1580 # not guarantee that uploading will not fail.
Edward Lemur125d60a2019-09-13 18:25:41 +00001581 self.EnsureAuthenticated(force=options.force)
1582 self.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001583
1584 # Apply watchlists on upload.
Edward Lemur2c62b332020-03-12 22:12:33 +00001585 watchlist = watchlists.Watchlists(settings.GetRoot())
1586 files = self.GetAffectedFiles(base_branch)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001587 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001588 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001589
Edward Lemur5a644f82020-03-18 16:44:57 +00001590 change_desc = self._GetDescriptionForUpload(options, git_diff_args, files)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001591 if not options.bypass_hooks:
Edward Lemur2c62b332020-03-12 22:12:33 +00001592 hook_results = self.RunHook(
1593 committing=False,
1594 may_prompt=not options.force,
1595 verbose=options.verbose,
1596 parallel=options.parallel,
1597 upstream=base_branch,
Edward Lemur5a644f82020-03-18 16:44:57 +00001598 description=change_desc.description,
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001599 all_files=False,
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001600 resultdb=options.resultdb,
1601 realm=options.realm)
Edward Lemur227d5102020-02-25 23:45:35 +00001602 self.ExtendCC(hook_results['more_cc'])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001603
Aaron Gable13101a62018-02-09 13:20:41 -08001604 print_stats(git_diff_args)
Edward Lemura12175c2020-03-09 16:58:26 +00001605 ret = self.CMDUploadChange(
Edward Lemur5a644f82020-03-18 16:44:57 +00001606 options, git_diff_args, custom_cl_base, change_desc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001607 if not ret:
Edward Lemur85153282020-02-14 22:06:29 +00001608 self._GitSetBranchConfigValue(
Gavin Makbe2e9262022-11-08 23:41:55 +00001609 LAST_UPLOAD_HASH_CONFIG_KEY,
1610 scm.GIT.ResolveCommit(settings.GetRoot(), 'HEAD'))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001611 # Run post upload hooks, if specified.
1612 if settings.GetRunPostUploadHook():
Brian Sheedy7326ca22022-11-02 18:36:17 +00001613 self.RunPostUploadHook(options.verbose, base_branch,
1614 change_desc.description,
1615 options.no_python2_post_upload_hooks)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001616
1617 # Upload all dependencies if specified.
1618 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001619 print()
1620 print('--dependencies has been specified.')
1621 print('All dependent local branches will be re-uploaded.')
1622 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001623 # Remove the dependencies flag from args so that we do not end up in a
1624 # loop.
1625 orig_args.remove('--dependencies')
Jose Lopes3863fc52020-04-07 17:00:25 +00001626 ret = upload_branch_deps(self, orig_args, options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001627 return ret
1628
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001629 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001630 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001631
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00001632 Issue must have been already uploaded and known. Optionally allows for
1633 updating Quick-Run (QR) state.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001634 """
1635 assert new_state in _CQState.ALL_STATES
1636 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001637 try:
Edward Lemur125d60a2019-09-13 18:25:41 +00001638 vote_map = {
1639 _CQState.NONE: 0,
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00001640 _CQState.QUICK_RUN: 1,
Edward Lemur125d60a2019-09-13 18:25:41 +00001641 _CQState.DRY_RUN: 1,
1642 _CQState.COMMIT: 2,
1643 }
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00001644 if new_state == _CQState.QUICK_RUN:
1645 labels = {
1646 'Commit-Queue': vote_map[_CQState.DRY_RUN],
1647 'Quick-Run': vote_map[_CQState.QUICK_RUN],
1648 }
1649 else:
1650 labels = {'Commit-Queue': vote_map[new_state]}
Edward Lemur125d60a2019-09-13 18:25:41 +00001651 notify = False if new_state == _CQState.DRY_RUN else None
1652 gerrit_util.SetReview(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001653 self.GetGerritHost(), self._GerritChangeIdentifier(),
Edward Lemur125d60a2019-09-13 18:25:41 +00001654 labels=labels, notify=notify)
qyearsley1fdfcb62016-10-24 13:22:03 -07001655 return 0
1656 except KeyboardInterrupt:
1657 raise
1658 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001659 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001660 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001661 ' * Your project has no CQ,\n'
1662 ' * You don\'t have permission to change the CQ state,\n'
1663 ' * There\'s a bug in this code (see stack trace below).\n'
1664 'Consider specifying which bots to trigger manually or asking your '
1665 'project owners for permissions or contacting Chrome Infra at:\n'
1666 'https://www.chromium.org/infra\n\n' %
1667 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001668 # Still raise exception so that stack trace is printed.
1669 raise
1670
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001671 def GetGerritHost(self):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001672 # Lazy load of configs.
1673 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07001674 if self._gerrit_host and '.' not in self._gerrit_host:
1675 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
1676 # This happens for internal stuff http://crbug.com/614312.
Edward Lemur79d4f992019-11-11 23:49:02 +00001677 parsed = urllib.parse.urlparse(self.GetRemoteUrl())
tandriie32e3ea2016-06-22 02:52:48 -07001678 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001679 print('WARNING: using non-https URLs for remote is likely broken\n'
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001680 ' Your current remote is: %s' % self.GetRemoteUrl())
tandriie32e3ea2016-06-22 02:52:48 -07001681 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
1682 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001683 return self._gerrit_host
1684
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001685 def _GetGitHost(self):
1686 """Returns git host to be used when uploading change to Gerrit."""
Edward Lemur298f2cf2019-02-22 21:40:39 +00001687 remote_url = self.GetRemoteUrl()
1688 if not remote_url:
1689 return None
Edward Lemur79d4f992019-11-11 23:49:02 +00001690 return urllib.parse.urlparse(remote_url).netloc
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001691
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001692 def GetCodereviewServer(self):
1693 if not self._gerrit_server:
1694 # If we're on a branch then get the server potentially associated
1695 # with that branch.
Edward Lemur85153282020-02-14 22:06:29 +00001696 if self.GetIssue() and self.GetBranch():
tandrii5d48c322016-08-18 16:19:37 -07001697 self._gerrit_server = self._GitGetBranchConfigValue(
Edward Lesmes50da7702020-03-30 19:23:43 +00001698 CODEREVIEW_SERVER_CONFIG_KEY)
tandrii5d48c322016-08-18 16:19:37 -07001699 if self._gerrit_server:
Edward Lemur79d4f992019-11-11 23:49:02 +00001700 self._gerrit_host = urllib.parse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001701 if not self._gerrit_server:
1702 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1703 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001704 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001705 parts[0] = parts[0] + '-review'
1706 self._gerrit_host = '.'.join(parts)
1707 self._gerrit_server = 'https://%s' % self._gerrit_host
1708 return self._gerrit_server
1709
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001710 def GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001711 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001712 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001713 if remote_url is None:
Josip906bfde2020-01-31 22:38:49 +00001714 logging.warning('can\'t detect Gerrit project.')
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001715 return None
Edward Lemur79d4f992019-11-11 23:49:02 +00001716 project = urllib.parse.urlparse(remote_url).path.strip('/')
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001717 if project.endswith('.git'):
1718 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00001719 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
1720 # 'a/' prefix, because 'a/' prefix is used to force authentication in
1721 # gitiles/git-over-https protocol. E.g.,
1722 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
1723 # as
1724 # https://chromium.googlesource.com/v8/v8
1725 if project.startswith('a/'):
1726 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001727 return project
1728
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001729 def _GerritChangeIdentifier(self):
1730 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
1731
1732 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001733 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001734 """
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001735 project = self.GetGerritProject()
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001736 if project:
1737 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
1738 # Fall back on still unique, but less efficient change number.
1739 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001740
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001741 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001742 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00001743 if settings.GetGerritSkipEnsureAuthenticated():
1744 # For projects with unusual authentication schemes.
1745 # See http://crbug.com/603378.
1746 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001747
1748 # Check presence of cookies only if using cookies-based auth method.
1749 cookie_auth = gerrit_util.Authenticator.get()
1750 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001751 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001752
Florian Mayerae510e82020-01-30 21:04:48 +00001753 remote_url = self.GetRemoteUrl()
1754 if remote_url is None:
Josip906bfde2020-01-31 22:38:49 +00001755 logging.warning('invalid remote')
Florian Mayerae510e82020-01-30 21:04:48 +00001756 return
Joanna Wang46ffd1b2022-09-16 20:44:44 +00001757 if urllib.parse.urlparse(remote_url).scheme not in ['https', 'sso']:
1758 logging.warning(
1759 'Ignoring branch %(branch)s with non-https/sso remote '
1760 '%(remote)s', {
1761 'branch': self.branch,
1762 'remote': self.GetRemoteUrl()
1763 })
Daniel Chengcf6269b2019-05-18 01:02:12 +00001764 return
1765
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001766 # Lazy-loader to identify Gerrit and Git hosts.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001767 self.GetCodereviewServer()
1768 git_host = self._GetGitHost()
Edward Lemur298f2cf2019-02-22 21:40:39 +00001769 assert self._gerrit_server and self._gerrit_host and git_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001770
1771 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
1772 git_auth = cookie_auth.get_auth_header(git_host)
1773 if gerrit_auth and git_auth:
1774 if gerrit_auth == git_auth:
1775 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001776 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
Raul Tambre80ee78e2019-05-06 22:41:05 +00001777 print(
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001778 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001779 ' %s\n'
1780 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02001781 ' Consider running the following command:\n'
1782 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001783 ' %s\n'
Raul Tambre80ee78e2019-05-06 22:41:05 +00001784 ' %s' %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02001785 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001786 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001787 cookie_auth.get_new_password_message(git_host)))
1788 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001789 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001790 return
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001791
1792 missing = (
1793 ([] if gerrit_auth else [self._gerrit_host]) +
1794 ([] if git_auth else [git_host]))
1795 DieWithError('Credentials for the following hosts are required:\n'
1796 ' %s\n'
1797 'These are read from %s (or legacy %s)\n'
1798 '%s' % (
1799 '\n '.join(missing),
1800 cookie_auth.get_gitcookies_path(),
1801 cookie_auth.get_netrc_path(),
1802 cookie_auth.get_new_password_message(git_host)))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001803
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001804 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001805 if not self.GetIssue():
1806 return
1807
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001808 status = self._GetChangeDetail()['status']
Joanna Wang583ca662022-04-27 21:17:17 +00001809 if status == 'ABANDONED':
Bruce Dawsoneb8426e2022-08-05 23:58:15 +00001810 DieWithError(
1811 'Change %s has been abandoned, new uploads are not allowed' %
1812 (self.GetIssueURL()))
Joanna Wang583ca662022-04-27 21:17:17 +00001813 if status == 'MERGED':
1814 answer = gclient_utils.AskForData(
1815 'Change %s has been submitted, new uploads are not allowed. '
1816 'Would you like to start a new change (Y/n)?' % self.GetIssueURL()
1817 ).lower()
1818 if answer not in ('y', ''):
1819 DieWithError('New uploads are not allowed.')
1820 self.SetIssue()
1821 return
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001822
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001823 # TODO(vadimsh): For some reason the chunk of code below was skipped if
1824 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
1825 # Apparently this check is not very important? Otherwise get_auth_email
1826 # could have been added to other implementations of Authenticator.
1827 cookies_auth = gerrit_util.Authenticator.get()
1828 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001829 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001830
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001831 cookies_user = cookies_auth.get_auth_email(self.GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001832 if self.GetIssueOwner() == cookies_user:
1833 return
1834 logging.debug('change %s owner is %s, cookies user is %s',
1835 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001836 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001837 # so ask what Gerrit thinks of this user.
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001838 details = gerrit_util.GetAccountDetails(self.GetGerritHost(), 'self')
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001839 if details['email'] == self.GetIssueOwner():
1840 return
1841 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001842 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001843 'as %s.\n'
1844 'Uploading may fail due to lack of permissions.' %
1845 (self.GetIssue(), self.GetIssueOwner(), details['email']))
1846 confirm_or_exit(action='upload')
1847
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001848 def GetStatus(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001849 """Applies a rough heuristic to give a simple summary of an issue's review
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001850 or CQ status, assuming adherence to a common workflow.
1851
1852 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001853 * 'error' - error from review tool (including deleted issues)
1854 * 'unsent' - no reviewers added
1855 * 'waiting' - waiting for review
1856 * 'reply' - waiting for uploader to reply to review
1857 * 'lgtm' - Code-Review label has been set
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00001858 * 'dry-run' - dry-running in the CQ
1859 * 'commit' - in the CQ
Aaron Gable9ab38c62017-04-06 14:36:33 -07001860 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001861 """
1862 if not self.GetIssue():
1863 return None
1864
1865 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001866 data = self._GetChangeDetail([
1867 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Edward Lemur79d4f992019-11-11 23:49:02 +00001868 except GerritChangeNotExists:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001869 return 'error'
1870
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00001871 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001872 return 'closed'
1873
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00001874 cq_label = data['labels'].get('Commit-Queue', {})
1875 max_cq_vote = 0
1876 for vote in cq_label.get('all', []):
1877 max_cq_vote = max(max_cq_vote, vote.get('value', 0))
1878 if max_cq_vote == 2:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001879 return 'commit'
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00001880 if max_cq_vote == 1:
1881 return 'dry-run'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001882
Aaron Gable9ab38c62017-04-06 14:36:33 -07001883 if data['labels'].get('Code-Review', {}).get('approved'):
1884 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001885
1886 if not data.get('reviewers', {}).get('REVIEWER', []):
1887 return 'unsent'
1888
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01001889 owner = data['owner'].get('_account_id')
Edward Lemur79d4f992019-11-11 23:49:02 +00001890 messages = sorted(data.get('messages', []), key=lambda m: m.get('date'))
Andrii Shyshkalov8aa9d622020-03-10 19:15:35 +00001891 while messages:
1892 m = messages.pop()
Andrii Shyshkalov899785a2021-07-09 12:45:37 +00001893 if (m.get('tag', '').startswith('autogenerated:cq') or
1894 m.get('tag', '').startswith('autogenerated:cv')):
1895 # Ignore replies from LUCI CV/CQ.
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01001896 continue
Andrii Shyshkalov8aa9d622020-03-10 19:15:35 +00001897 if m.get('author', {}).get('_account_id') == owner:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001898 # Most recent message was by owner.
1899 return 'waiting'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001900
1901 # Some reply from non-owner.
1902 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07001903
1904 # Somehow there are no messages even though there are reviewers.
1905 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001906
Gavin Mak4e5e3992022-11-14 22:40:12 +00001907 def GetMostRecentPatchset(self, update=True):
Edward Lemur6c6827c2020-02-06 21:15:18 +00001908 if not self.GetIssue():
1909 return None
1910
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001911 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08001912 patchset = data['revisions'][data['current_revision']]['_number']
Gavin Mak4e5e3992022-11-14 22:40:12 +00001913 if update:
1914 self.SetPatchset(patchset)
Aaron Gablee8856ee2017-12-07 12:41:46 -08001915 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001916
Gavin Makf35a9eb2022-11-17 18:34:36 +00001917 def _IsPatchsetRangeSignificant(self, lower, upper):
1918 """Returns True if the inclusive range of patchsets contains any reworks or
1919 rebases."""
1920 if not self.GetIssue():
1921 return False
1922
1923 data = self._GetChangeDetail(['ALL_REVISIONS'])
1924 ps_kind = {}
1925 for rev_info in data.get('revisions', {}).values():
1926 ps_kind[rev_info['_number']] = rev_info.get('kind', '')
1927
1928 for ps in range(lower, upper + 1):
1929 assert ps in ps_kind, 'expected patchset %d in change detail' % ps
1930 if ps_kind[ps] not in ('NO_CHANGE', 'NO_CODE_CHANGE'):
1931 return True
1932 return False
1933
Gavin Make61ccc52020-11-13 00:12:57 +00001934 def GetMostRecentDryRunPatchset(self):
1935 """Get patchsets equivalent to the most recent patchset and return
1936 the patchset with the latest dry run. If none have been dry run, return
1937 the latest patchset."""
1938 if not self.GetIssue():
1939 return None
1940
1941 data = self._GetChangeDetail(['ALL_REVISIONS'])
1942 patchset = data['revisions'][data['current_revision']]['_number']
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001943 dry_run = {int(m['_revision_number'])
1944 for m in data.get('messages', [])
1945 if m.get('tag', '').endswith('dry-run')}
Gavin Make61ccc52020-11-13 00:12:57 +00001946
1947 for revision_info in sorted(data.get('revisions', {}).values(),
1948 key=lambda c: c['_number'], reverse=True):
1949 if revision_info['_number'] in dry_run:
1950 patchset = revision_info['_number']
1951 break
1952 if revision_info.get('kind', '') not in \
1953 ('NO_CHANGE', 'NO_CODE_CHANGE', 'TRIVIAL_REBASE'):
1954 break
1955 self.SetPatchset(patchset)
1956 return patchset
1957
Aaron Gable636b13f2017-07-14 10:42:48 -07001958 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001959 gerrit_util.SetReview(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001960 self.GetGerritHost(), self._GerritChangeIdentifier(),
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001961 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001962
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001963 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01001964 # DETAILED_ACCOUNTS is to get emails in accounts.
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001965 # CURRENT_REVISION is included to get the latest patchset so that
1966 # only the robot comments from the latest patchset can be shown.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001967 messages = self._GetChangeDetail(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001968 options=['MESSAGES', 'DETAILED_ACCOUNTS',
1969 'CURRENT_REVISION']).get('messages', [])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001970 file_comments = gerrit_util.GetChangeComments(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001971 self.GetGerritHost(), self._GerritChangeIdentifier())
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001972 robot_file_comments = gerrit_util.GetChangeRobotComments(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001973 self.GetGerritHost(), self._GerritChangeIdentifier())
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001974
1975 # Add the robot comments onto the list of comments, but only
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +00001976 # keep those that are from the latest patchset.
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001977 latest_patch_set = self.GetMostRecentPatchset()
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001978 for path, robot_comments in robot_file_comments.items():
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001979 line_comments = file_comments.setdefault(path, [])
1980 line_comments.extend(
1981 [c for c in robot_comments if c['patch_set'] == latest_patch_set])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001982
1983 # Build dictionary of file comments for easy access and sorting later.
1984 # {author+date: {path: {patchset: {line: url+message}}}}
1985 comments = collections.defaultdict(
1986 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
Andrii Shyshkalova3762a92020-11-25 10:20:42 +00001987
1988 server = self.GetCodereviewServer()
1989 if server in _KNOWN_GERRIT_TO_SHORT_URLS:
1990 # /c/ is automatically added by short URL server.
1991 url_prefix = '%s/%s' % (_KNOWN_GERRIT_TO_SHORT_URLS[server],
1992 self.GetIssue())
1993 else:
1994 url_prefix = '%s/c/%s' % (server, self.GetIssue())
1995
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001996 for path, line_comments in file_comments.items():
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001997 for comment in line_comments:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001998 tag = comment.get('tag', '')
1999 if tag.startswith('autogenerated') and 'robot_id' not in comment:
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002000 continue
2001 key = (comment['author']['email'], comment['updated'])
2002 if comment.get('side', 'REVISION') == 'PARENT':
2003 patchset = 'Base'
2004 else:
2005 patchset = 'PS%d' % comment['patch_set']
2006 line = comment.get('line', 0)
Andrii Shyshkalova3762a92020-11-25 10:20:42 +00002007 url = ('%s/%s/%s#%s%s' %
2008 (url_prefix, comment['patch_set'], path,
2009 'b' if comment.get('side') == 'PARENT' else '',
2010 str(line) if line else ''))
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002011 comments[key][path][patchset][line] = (url, comment['message'])
2012
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002013 summaries = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002014 for msg in messages:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002015 summary = self._BuildCommentSummary(msg, comments, readable)
2016 if summary:
2017 summaries.append(summary)
2018 return summaries
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002019
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002020 @staticmethod
2021 def _BuildCommentSummary(msg, comments, readable):
Josip Sokcevic266129c2021-11-09 00:22:00 +00002022 if 'email' not in msg['author']:
2023 # Some bot accounts may not have an email associated.
2024 return None
2025
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002026 key = (msg['author']['email'], msg['date'])
2027 # Don't bother showing autogenerated messages that don't have associated
2028 # file or line comments. this will filter out most autogenerated
2029 # messages, but will keep robot comments like those from Tricium.
2030 is_autogenerated = msg.get('tag', '').startswith('autogenerated')
2031 if is_autogenerated and not comments.get(key):
2032 return None
2033 message = msg['message']
2034 # Gerrit spits out nanoseconds.
2035 assert len(msg['date'].split('.')[-1]) == 9
2036 date = datetime.datetime.strptime(msg['date'][:-3],
2037 '%Y-%m-%d %H:%M:%S.%f')
2038 if key in comments:
2039 message += '\n'
2040 for path, patchsets in sorted(comments.get(key, {}).items()):
2041 if readable:
2042 message += '\n%s' % path
2043 for patchset, lines in sorted(patchsets.items()):
2044 for line, (url, content) in sorted(lines.items()):
2045 if line:
2046 line_str = 'Line %d' % line
2047 path_str = '%s:%d:' % (path, line)
2048 else:
2049 line_str = 'File comment'
2050 path_str = '%s:0:' % path
2051 if readable:
2052 message += '\n %s, %s: %s' % (patchset, line_str, url)
2053 message += '\n %s\n' % content
2054 else:
2055 message += '\n%s ' % path_str
2056 message += '\n%s\n' % content
2057
2058 return _CommentSummary(
2059 date=date,
2060 message=message,
2061 sender=msg['author']['email'],
2062 autogenerated=is_autogenerated,
2063 # These could be inferred from the text messages and correlated with
2064 # Code-Review label maximum, however this is not reliable.
2065 # Leaving as is until the need arises.
2066 approval=False,
2067 disapproval=False,
2068 )
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002069
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002070 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002071 gerrit_util.AbandonChange(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002072 self.GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002073
Xinan Lin1bd4ffa2021-07-28 00:54:22 +00002074 def SubmitIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002075 gerrit_util.SubmitChange(
Xinan Lin1bd4ffa2021-07-28 00:54:22 +00002076 self.GetGerritHost(), self._GerritChangeIdentifier())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002077
Edward Lesmes7677e5c2020-02-19 20:39:03 +00002078 def _GetChangeDetail(self, options=None):
2079 """Returns details of associated Gerrit change and caching results."""
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002080 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002081 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002082
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002083 # Optimization to avoid multiple RPCs:
Edward Lesmes7677e5c2020-02-19 20:39:03 +00002084 if 'CURRENT_REVISION' in options or 'ALL_REVISIONS' in options:
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002085 options.append('CURRENT_COMMIT')
2086
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002087 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002088 cache_key = str(self.GetIssue())
Edward Lesmes7677e5c2020-02-19 20:39:03 +00002089 options_set = frozenset(o.upper() for o in options)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002090
Edward Lesmes7677e5c2020-02-19 20:39:03 +00002091 for cached_options_set, data in self._detail_cache.get(cache_key, []):
2092 # Assumption: data fetched before with extra options is suitable
2093 # for return for a smaller set of options.
2094 # For example, if we cached data for
2095 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2096 # and request is for options=[CURRENT_REVISION],
2097 # THEN we can return prior cached data.
2098 if options_set.issubset(cached_options_set):
2099 return data
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002100
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002101 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002102 data = gerrit_util.GetChangeDetail(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002103 self.GetGerritHost(), self._GerritChangeIdentifier(), options_set)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002104 except gerrit_util.GerritError as e:
2105 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002106 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002107 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002108
Edward Lesmes7677e5c2020-02-19 20:39:03 +00002109 self._detail_cache.setdefault(cache_key, []).append((options_set, data))
tandriic2405f52016-10-10 08:13:15 -07002110 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002111
Gavin Mak4e5e3992022-11-14 22:40:12 +00002112 def _GetChangeCommit(self, revision='current'):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002113 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002114 try:
Gavin Mak4e5e3992022-11-14 22:40:12 +00002115 data = gerrit_util.GetChangeCommit(self.GetGerritHost(),
2116 self._GerritChangeIdentifier(),
2117 revision)
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002118 except gerrit_util.GerritError as e:
2119 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002120 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002121 raise
agable32978d92016-11-01 12:55:02 -07002122 return data
2123
Karen Qian40c19422019-03-13 21:28:29 +00002124 def _IsCqConfigured(self):
2125 detail = self._GetChangeDetail(['LABELS'])
Andrii Shyshkalov8effa4d2020-01-21 13:23:36 +00002126 return u'Commit-Queue' in detail.get('labels', {})
Karen Qian40c19422019-03-13 21:28:29 +00002127
Saagar Sanghavi03b15132020-08-10 16:43:41 +00002128 def CMDLand(self, force, bypass_hooks, verbose, parallel, resultdb, realm):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002129 if git_common.is_dirty_git_tree('land'):
2130 return 1
Karen Qian40c19422019-03-13 21:28:29 +00002131
tandriid60367b2016-06-22 05:25:12 -07002132 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
Karen Qian40c19422019-03-13 21:28:29 +00002133 if not force and self._IsCqConfigured():
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00002134 confirm_or_exit('\nIt seems this repository has a CQ, '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002135 'which can test and land changes for you. '
2136 'Are you sure you wish to bypass it?\n',
2137 action='bypass CQ')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002138 differs = True
Gavin Makbe2e9262022-11-08 23:41:55 +00002139 last_upload = self._GitGetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002140 # Note: git diff outputs nothing if there is no diff.
2141 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002142 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002143 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002144 if detail['current_revision'] == last_upload:
2145 differs = False
2146 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002147 print('WARNING: Local branch contents differ from latest uploaded '
2148 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002149 if differs:
2150 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002151 confirm_or_exit(
2152 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2153 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002154 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002155 elif not bypass_hooks:
Edward Lemur227d5102020-02-25 23:45:35 +00002156 upstream = self.GetCommonAncestorWithUpstream()
2157 if self.GetIssue():
2158 description = self.FetchDescription()
2159 else:
Edward Lemura12175c2020-03-09 16:58:26 +00002160 description = _create_description_from_log([upstream])
Edward Lemur227d5102020-02-25 23:45:35 +00002161 self.RunHook(
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002162 committing=True,
2163 may_prompt=not force,
2164 verbose=verbose,
Edward Lemur227d5102020-02-25 23:45:35 +00002165 parallel=parallel,
2166 upstream=upstream,
2167 description=description,
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00002168 all_files=False,
Saagar Sanghavi03b15132020-08-10 16:43:41 +00002169 resultdb=resultdb,
2170 realm=realm)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002171
Xinan Lin1bd4ffa2021-07-28 00:54:22 +00002172 self.SubmitIssue()
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002173 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002174 links = self._GetChangeCommit().get('web_links', [])
2175 for link in links:
Michael Mosse371c642021-09-29 16:41:04 +00002176 if link.get('name') in ['gitiles', 'browse'] and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002177 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002178 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002179 return 0
2180
Bruce Dawsonf362f6f2021-02-18 23:15:17 +00002181 def CMDPatchWithParsedIssue(self, parsed_issue_arg, nocommit, force,
2182 newbranch):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002183 assert parsed_issue_arg.valid
2184
Edward Lemur125d60a2019-09-13 18:25:41 +00002185 self.issue = parsed_issue_arg.issue
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002186
2187 if parsed_issue_arg.hostname:
2188 self._gerrit_host = parsed_issue_arg.hostname
2189 self._gerrit_server = 'https://%s' % self._gerrit_host
2190
tandriic2405f52016-10-10 08:13:15 -07002191 try:
2192 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002193 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002194 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002195
2196 if not parsed_issue_arg.patchset:
2197 # Use current revision by default.
2198 revision_info = detail['revisions'][detail['current_revision']]
2199 patchset = int(revision_info['_number'])
2200 else:
2201 patchset = parsed_issue_arg.patchset
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002202 for revision_info in detail['revisions'].values():
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002203 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2204 break
2205 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002206 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002207 (parsed_issue_arg.patchset, self.GetIssue()))
2208
Edward Lemur125d60a2019-09-13 18:25:41 +00002209 remote_url = self.GetRemoteUrl()
Aaron Gable697a91b2018-01-19 15:20:15 -08002210 if remote_url.endswith('.git'):
2211 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002212 remote_url = remote_url.rstrip('/')
2213
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002214 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002215 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002216
2217 if remote_url != fetch_info['url']:
2218 DieWithError('Trying to patch a change from %s but this repo appears '
2219 'to be %s.' % (fetch_info['url'], remote_url))
2220
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002221 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002222
Bruce Dawsonf362f6f2021-02-18 23:15:17 +00002223 # If we have created a new branch then do the "set issue" immediately in
2224 # case the cherry-pick fails, which happens when resolving conflicts.
2225 if newbranch:
2226 self.SetIssue(parsed_issue_arg.issue)
2227
Aaron Gable62619a32017-06-16 08:22:09 -07002228 if force:
2229 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2230 print('Checked out commit for change %i patchset %i locally' %
2231 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002232 elif nocommit:
2233 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2234 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002235 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002236 RunGit(['cherry-pick', 'FETCH_HEAD'])
2237 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002238 (parsed_issue_arg.issue, patchset))
2239 print('Note: this created a local commit which does not have '
2240 'the same hash as the one uploaded for review. This will make '
2241 'uploading changes based on top of this branch difficult.\n'
2242 'If you want to do that, use "git cl patch --force" instead.')
2243
Stefan Zagerd08043c2017-10-12 12:07:02 -07002244 if self.GetBranch():
2245 self.SetIssue(parsed_issue_arg.issue)
2246 self.SetPatchset(patchset)
Edward Lesmes50da7702020-03-30 19:23:43 +00002247 fetched_hash = scm.GIT.ResolveCommit(settings.GetRoot(), 'FETCH_HEAD')
Gavin Makbe2e9262022-11-08 23:41:55 +00002248 self._GitSetBranchConfigValue(LAST_UPLOAD_HASH_CONFIG_KEY, fetched_hash)
2249 self._GitSetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY, fetched_hash)
Stefan Zagerd08043c2017-10-12 12:07:02 -07002250 else:
2251 print('WARNING: You are in detached HEAD state.\n'
2252 'The patch has been applied to your checkout, but you will not be '
2253 'able to upload a new patch set to the gerrit issue.\n'
2254 'Try using the \'-b\' option if you would like to work on a '
2255 'branch and/or upload a new patch set.')
2256
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002257 return 0
2258
Joanna Wang18de1f62023-01-21 01:24:24 +00002259 @staticmethod
2260 def _GerritCommitMsgHookCheck(offer_removal):
2261 # type: (bool) -> None
2262 """Checks for the gerrit's commit-msg hook and removes it if necessary."""
tandrii16e0b4e2016-06-07 10:34:28 -07002263 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2264 if not os.path.exists(hook):
2265 return
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002266 # Crude attempt to distinguish Gerrit Codereview hook from a potentially
2267 # custom developer-made one.
tandrii16e0b4e2016-06-07 10:34:28 -07002268 data = gclient_utils.FileRead(hook)
2269 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2270 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002271 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002272 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002273 'and may interfere with it in subtle ways.\n'
2274 'We recommend you remove the commit-msg hook.')
2275 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002276 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002277 gclient_utils.rm_file_or_tree(hook)
2278 print('Gerrit commit-msg hook removed.')
2279 else:
2280 print('OK, will keep Gerrit commit-msg hook in place.')
2281
Edward Lemur1b52d872019-05-09 21:12:12 +00002282 def _CleanUpOldTraces(self):
2283 """Keep only the last |MAX_TRACES| traces."""
2284 try:
2285 traces = sorted([
2286 os.path.join(TRACES_DIR, f)
2287 for f in os.listdir(TRACES_DIR)
2288 if (os.path.isfile(os.path.join(TRACES_DIR, f))
2289 and not f.startswith('tmp'))
2290 ])
2291 traces_to_delete = traces[:-MAX_TRACES]
2292 for trace in traces_to_delete:
Daniel Chengcf6269b2019-05-18 01:02:12 +00002293 os.remove(trace)
Edward Lemur1b52d872019-05-09 21:12:12 +00002294 except OSError:
2295 print('WARNING: Failed to remove old git traces from\n'
2296 ' %s'
2297 'Consider removing them manually.' % TRACES_DIR)
Edward Lemurdc8e23d2019-05-07 00:45:48 +00002298
Edward Lemur5737f022019-05-17 01:24:00 +00002299 def _WriteGitPushTraces(self, trace_name, traces_dir, git_push_metadata):
Edward Lemur1b52d872019-05-09 21:12:12 +00002300 """Zip and write the git push traces stored in traces_dir."""
2301 gclient_utils.safe_makedirs(TRACES_DIR)
Edward Lemur1b52d872019-05-09 21:12:12 +00002302 traces_zip = trace_name + '-traces'
2303 traces_readme = trace_name + '-README'
Michael Mosse7f0b4c2019-05-08 04:36:24 +00002304 # Create a temporary dir to store git config and gitcookies in. It will be
2305 # compressed and stored next to the traces.
2306 git_info_dir = tempfile.mkdtemp()
Edward Lemur1b52d872019-05-09 21:12:12 +00002307 git_info_zip = trace_name + '-git-info'
2308
Josip Sokcevic5e18b602020-04-23 21:47:00 +00002309 git_push_metadata['now'] = datetime_now().strftime('%Y-%m-%dT%H:%M:%S.%f')
sangwoo.ko7a614332019-05-22 02:46:19 +00002310
Edward Lemur1b52d872019-05-09 21:12:12 +00002311 git_push_metadata['trace_name'] = trace_name
2312 gclient_utils.FileWrite(
2313 traces_readme, TRACES_README_FORMAT % git_push_metadata)
2314
2315 # Keep only the first 6 characters of the git hashes on the packet
2316 # trace. This greatly decreases size after compression.
2317 packet_traces = os.path.join(traces_dir, 'trace-packet')
2318 if os.path.isfile(packet_traces):
2319 contents = gclient_utils.FileRead(packet_traces)
2320 gclient_utils.FileWrite(
2321 packet_traces, GIT_HASH_RE.sub(r'\1', contents))
2322 shutil.make_archive(traces_zip, 'zip', traces_dir)
2323
2324 # Collect and compress the git config and gitcookies.
2325 git_config = RunGit(['config', '-l'])
2326 gclient_utils.FileWrite(
2327 os.path.join(git_info_dir, 'git-config'),
2328 git_config)
2329
2330 cookie_auth = gerrit_util.Authenticator.get()
2331 if isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
2332 gitcookies_path = cookie_auth.get_gitcookies_path()
2333 if os.path.isfile(gitcookies_path):
2334 gitcookies = gclient_utils.FileRead(gitcookies_path)
2335 gclient_utils.FileWrite(
2336 os.path.join(git_info_dir, 'gitcookies'),
2337 GITCOOKIES_REDACT_RE.sub('REDACTED', gitcookies))
2338 shutil.make_archive(git_info_zip, 'zip', git_info_dir)
2339
Edward Lemur1b52d872019-05-09 21:12:12 +00002340 gclient_utils.rmtree(git_info_dir)
2341
Josip Sokcevicf2cfd3d2021-03-30 18:39:18 +00002342 def _RunGitPushWithTraces(self,
2343 refspec,
2344 refspec_opts,
2345 git_push_metadata,
2346 git_push_options=None):
Edward Lemur1b52d872019-05-09 21:12:12 +00002347 """Run git push and collect the traces resulting from the execution."""
2348 # Create a temporary directory to store traces in. Traces will be compressed
2349 # and stored in a 'traces' dir inside depot_tools.
2350 traces_dir = tempfile.mkdtemp()
Edward Lemur5737f022019-05-17 01:24:00 +00002351 trace_name = os.path.join(
2352 TRACES_DIR, datetime_now().strftime('%Y%m%dT%H%M%S.%f'))
Edward Lemur0f58ae42019-04-30 17:24:12 +00002353
2354 env = os.environ.copy()
2355 env['GIT_REDACT_COOKIES'] = 'o,SSO,GSSO_Uberproxy'
2356 env['GIT_TR2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Jonathan Nieder9779b142019-05-29 23:19:29 +00002357 env['GIT_TRACE2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Edward Lemur0f58ae42019-04-30 17:24:12 +00002358 env['GIT_TRACE_CURL'] = os.path.join(traces_dir, 'trace-curl')
2359 env['GIT_TRACE_CURL_NO_DATA'] = '1'
2360 env['GIT_TRACE_PACKET'] = os.path.join(traces_dir, 'trace-packet')
2361
2362 try:
2363 push_returncode = 0
Edward Lemur1b52d872019-05-09 21:12:12 +00002364 remote_url = self.GetRemoteUrl()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002365 before_push = time_time()
Josip Sokcevicf2cfd3d2021-03-30 18:39:18 +00002366 push_cmd = ['git', 'push', remote_url, refspec]
2367 if git_push_options:
2368 for opt in git_push_options:
2369 push_cmd.extend(['-o', opt])
2370
Edward Lemur0f58ae42019-04-30 17:24:12 +00002371 push_stdout = gclient_utils.CheckCallAndFilter(
Josip Sokcevicf2cfd3d2021-03-30 18:39:18 +00002372 push_cmd,
Edward Lemur0f58ae42019-04-30 17:24:12 +00002373 env=env,
2374 print_stdout=True,
2375 # Flush after every line: useful for seeing progress when running as
2376 # recipe.
2377 filter_fn=lambda _: sys.stdout.flush())
Edward Lemur79d4f992019-11-11 23:49:02 +00002378 push_stdout = push_stdout.decode('utf-8', 'replace')
Edward Lemur0f58ae42019-04-30 17:24:12 +00002379 except subprocess2.CalledProcessError as e:
2380 push_returncode = e.returncode
Aravind Vasudevanc9508582022-10-18 03:07:41 +00002381 if 'blocked keyword' in str(e.stdout) or 'banned word' in str(e.stdout):
Josip Sokcevic740825e2021-05-12 18:28:34 +00002382 raise GitPushError(
2383 'Failed to create a change, very likely due to blocked keyword. '
2384 'Please examine output above for the reason of the failure.\n'
2385 'If this is a false positive, you can try to bypass blocked '
2386 'keyword by using push option '
2387 '-o uploadvalidator~skip, e.g.:\n'
2388 'git cl upload -o uploadvalidator~skip\n\n'
2389 'If git-cl is not working correctly, file a bug under the '
2390 'Infra>SDK component.')
Josip Sokcevic54e30e72022-02-10 22:32:24 +00002391 if 'git push -o nokeycheck' in str(e.stdout):
2392 raise GitPushError(
2393 'Failed to create a change, very likely due to a private key being '
2394 'detected. Please examine output above for the reason of the '
2395 'failure.\n'
2396 'If this is a false positive, you can try to bypass private key '
2397 'detection by using push option '
2398 '-o nokeycheck, e.g.:\n'
2399 'git cl upload -o nokeycheck\n\n'
2400 'If git-cl is not working correctly, file a bug under the '
2401 'Infra>SDK component.')
Josip Sokcevic740825e2021-05-12 18:28:34 +00002402
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002403 raise GitPushError(
2404 'Failed to create a change. Please examine output above for the '
2405 'reason of the failure.\n'
Josip Sokcevic7386a1e2021-02-12 19:00:34 +00002406 'For emergencies, Googlers can escalate to '
2407 'go/gob-support or go/notify#gob\n'
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002408 'Hint: run command below to diagnose common Git/Gerrit '
2409 'credential problems:\n'
2410 ' git cl creds-check\n'
2411 '\n'
2412 'If git-cl is not working correctly, file a bug under the Infra>SDK '
2413 'component including the files below.\n'
2414 'Review the files before upload, since they might contain sensitive '
2415 'information.\n'
2416 'Set the Restrict-View-Google label so that they are not publicly '
2417 'accessible.\n' + TRACES_MESSAGE % {'trace_name': trace_name})
Edward Lemur0f58ae42019-04-30 17:24:12 +00002418 finally:
2419 execution_time = time_time() - before_push
2420 metrics.collector.add_repeated('sub_commands', {
2421 'command': 'git push',
2422 'execution_time': execution_time,
2423 'exit_code': push_returncode,
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002424 'arguments': metrics_utils.extract_known_subcommand_args(refspec_opts),
Edward Lemur0f58ae42019-04-30 17:24:12 +00002425 })
2426
Edward Lemur1b52d872019-05-09 21:12:12 +00002427 git_push_metadata['execution_time'] = execution_time
2428 git_push_metadata['exit_code'] = push_returncode
Edward Lemur5737f022019-05-17 01:24:00 +00002429 self._WriteGitPushTraces(trace_name, traces_dir, git_push_metadata)
Edward Lemur0f58ae42019-04-30 17:24:12 +00002430
Edward Lemur1b52d872019-05-09 21:12:12 +00002431 self._CleanUpOldTraces()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002432 gclient_utils.rmtree(traces_dir)
2433
2434 return push_stdout
2435
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002436 def CMDUploadChange(self, options, git_diff_args, custom_cl_base,
2437 change_desc):
2438 """Upload the current branch to Gerrit, retry if new remote HEAD is
2439 found. options and change_desc may be mutated."""
Josip Sokcevicb631a882021-01-06 18:18:10 +00002440 remote, remote_branch = self.GetRemoteBranch()
2441 branch = GetTargetRef(remote, remote_branch, options.target_branch)
2442
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002443 try:
2444 return self._CMDUploadChange(options, git_diff_args, custom_cl_base,
Josip Sokcevicb631a882021-01-06 18:18:10 +00002445 change_desc, branch)
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002446 except GitPushError as e:
Josip Sokcevicb631a882021-01-06 18:18:10 +00002447 # Repository might be in the middle of transition to main branch as
2448 # default, and uploads to old default might be blocked.
2449 if remote_branch not in [DEFAULT_OLD_BRANCH, DEFAULT_NEW_BRANCH]:
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002450 DieWithError(str(e), change_desc)
2451
Josip Sokcevicb631a882021-01-06 18:18:10 +00002452 project_head = gerrit_util.GetProjectHead(self._gerrit_host,
2453 self.GetGerritProject())
2454 if project_head == branch:
2455 DieWithError(str(e), change_desc)
2456 branch = project_head
2457
2458 print("WARNING: Fetching remote state and retrying upload to default "
2459 "branch...")
2460 RunGit(['fetch', '--prune', remote])
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002461 options.edit_description = False
2462 options.force = True
2463 try:
Josip Sokcevicb631a882021-01-06 18:18:10 +00002464 self._CMDUploadChange(options, git_diff_args, custom_cl_base,
2465 change_desc, branch)
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002466 except GitPushError as e:
2467 DieWithError(str(e), change_desc)
2468
2469 def _CMDUploadChange(self, options, git_diff_args, custom_cl_base,
Josip Sokcevicb631a882021-01-06 18:18:10 +00002470 change_desc, branch):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002471 """Upload the current branch to Gerrit."""
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002472 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002473 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
Gavin Mak4e5e3992022-11-14 22:40:12 +00002474 external_parent = None
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002475 if self.GetIssue():
Josipe827b0f2020-01-30 00:07:20 +00002476 # User requested to change description
2477 if options.edit_description:
Josipe827b0f2020-01-30 00:07:20 +00002478 change_desc.prompt()
Gavin Mak4e5e3992022-11-14 22:40:12 +00002479 change_detail = self._GetChangeDetail(['CURRENT_REVISION'])
2480 change_id = change_detail['change_id']
Edward Lemur5a644f82020-03-18 16:44:57 +00002481 change_desc.ensure_change_id(change_id)
Gavin Mak4e5e3992022-11-14 22:40:12 +00002482
2483 # Check if changes outside of this workspace have been uploaded.
2484 current_rev = change_detail['current_revision']
2485 last_uploaded_rev = self._GitGetBranchConfigValue(
2486 GERRIT_SQUASH_HASH_CONFIG_KEY)
2487 if last_uploaded_rev and current_rev != last_uploaded_rev:
2488 external_parent = self._UpdateWithExternalChanges()
Aaron Gableb56ad332017-01-06 15:24:31 -08002489 else: # if not self.GetIssue()
Gavin Mak68e6cf32021-01-25 18:24:08 +00002490 if not options.force and not options.message_file:
Anthony Polito8b955342019-09-24 19:01:36 +00002491 change_desc.prompt()
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002492 change_ids = git_footers.get_footer_change_id(change_desc.description)
Edward Lemur5a644f82020-03-18 16:44:57 +00002493 if len(change_ids) == 1:
2494 change_id = change_ids[0]
2495 else:
2496 change_id = GenerateGerritChangeId(change_desc.description)
2497 change_desc.ensure_change_id(change_id)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002498
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00002499 if options.preserve_tryjobs:
2500 change_desc.set_preserve_tryjobs()
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002501
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002502 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Gavin Mak4e5e3992022-11-14 22:40:12 +00002503 parent = external_parent or self._ComputeParent(
Edward Lemur5a644f82020-03-18 16:44:57 +00002504 remote, upstream_branch, custom_cl_base, options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002505 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Edward Lemur1773f372020-02-22 00:27:14 +00002506 with gclient_utils.temporary_file() as desc_tempfile:
2507 gclient_utils.FileWrite(desc_tempfile, change_desc.description)
2508 ref_to_push = RunGit(
2509 ['commit-tree', tree, '-p', parent, '-F', desc_tempfile]).strip()
Anthony Polito8b955342019-09-24 19:01:36 +00002510 else: # if not options.squash
Gregory Nisbet48d9e1e2021-04-15 23:35:54 +00002511 if options.no_add_changeid:
2512 pass
2513 else: # adding Change-Ids is okay.
2514 if not git_footers.get_footer_change_id(change_desc.description):
2515 DownloadGerritHook(False)
2516 change_desc.set_description(
2517 self._AddChangeIdToCommitMessage(change_desc.description,
2518 git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002519 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002520 # For no-squash mode, we assume the remote called "origin" is the one we
2521 # want. It is not worthwhile to support different workflows for
2522 # no-squash mode.
2523 parent = 'origin/%s' % branch
Gregory Nisbet48d9e1e2021-04-15 23:35:54 +00002524 # attempt to extract the changeid from the current description
2525 # fail informatively if not possible.
2526 change_id_candidates = git_footers.get_footer_change_id(
2527 change_desc.description)
2528 if not change_id_candidates:
2529 DieWithError("Unable to extract change-id from message.")
2530 change_id = change_id_candidates[0]
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002531
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00002532 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002533 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2534 ref_to_push)]).splitlines()
2535 if len(commits) > 1:
2536 print('WARNING: This will upload %d commits. Run the following command '
2537 'to see which commits will be uploaded: ' % len(commits))
2538 print('git log %s..%s' % (parent, ref_to_push))
2539 print('You can also use `git squash-branch` to squash these into a '
2540 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002541 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002542
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002543 reviewers = sorted(change_desc.get_reviewers())
Edward Lemur4508b422019-10-03 21:56:35 +00002544 cc = []
Joanna Wangc8f23e22023-01-19 21:18:10 +00002545 # Add CCs from WATCHLISTS and rietveld.cc git config unless this is
Edward Lemur4508b422019-10-03 21:56:35 +00002546 # the initial upload, the CL is private, or auto-CCing has ben disabled.
2547 if not (self.GetIssue() or options.private or options.no_autocc):
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002548 cc = self.GetCCList().split(',')
Gavin Makb1c08f62021-04-01 18:05:58 +00002549 if len(cc) > 100:
2550 lsc = ('https://chromium.googlesource.com/chromium/src/+/HEAD/docs/'
2551 'process/lsc/lsc_workflow.md')
2552 print('WARNING: This will auto-CC %s users.' % len(cc))
2553 print('LSC may be more appropriate: %s' % lsc)
2554 print('You can also use the --no-autocc flag to disable auto-CC.')
2555 confirm_or_exit(action='continue')
Edward Lemur4508b422019-10-03 21:56:35 +00002556 # Add cc's from the --cc flag.
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002557 if options.cc:
2558 cc.extend(options.cc)
Edward Lemur79d4f992019-11-11 23:49:02 +00002559 cc = [email.strip() for email in cc if email.strip()]
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002560 if change_desc.get_cced():
2561 cc.extend(change_desc.get_cced())
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002562 if self.GetGerritHost() == 'chromium-review.googlesource.com':
Andrii Shyshkalov0da5e8f2018-10-30 17:29:18 +00002563 valid_accounts = set(reviewers + cc)
2564 # TODO(crbug/877717): relax this for all hosts.
2565 else:
2566 valid_accounts = gerrit_util.ValidAccounts(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002567 self.GetGerritHost(), reviewers + cc)
Andrii Shyshkalovf170af42018-10-30 07:00:44 +00002568 logging.info('accounts %s are recognized, %s invalid',
2569 sorted(valid_accounts),
2570 set(reviewers + cc).difference(set(valid_accounts)))
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002571
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002572 # Extra options that can be specified at push time. Doc:
2573 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002574 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002575
Aaron Gable844cf292017-06-28 11:32:59 -07002576 # By default, new changes are started in WIP mode, and subsequent patchsets
Thiago Perrottab0fb8d52022-08-30 21:26:19 +00002577 # don't send email. At any time, passing --send-mail or --send-email will
2578 # mark the change ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07002579 if options.send_mail:
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002580 refspec_opts.append('ready')
2581 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04002582 elif not self.GetIssue() and options.squash:
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002583 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07002584 else:
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002585 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07002586
Thiago Perrottab0fb8d52022-08-30 21:26:19 +00002587 # TODO(tandrii): options.message should be posted as a comment if
2588 # --send-mail or --send-email is set on non-initial upload as Rietveld used
2589 # to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002590
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002591 # Set options.title in case user was prompted in _GetTitleForUpload and
2592 # _CMDUploadChange needs to be called again.
2593 options.title = self._GetTitleForUpload(options)
2594 if options.title:
Nick Carter8692b182017-11-06 16:30:38 -08002595 # Punctuation and whitespace in |title| must be percent-encoded.
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002596 refspec_opts.append(
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002597 'm=' + gerrit_util.PercentEncodeForGitRef(options.title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002598
agablec6787972016-09-09 16:13:34 -07002599 if options.private:
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002600 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07002601
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002602 for r in sorted(reviewers):
2603 if r in valid_accounts:
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002604 refspec_opts.append('r=%s' % r)
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002605 reviewers.remove(r)
2606 else:
2607 # TODO(tandrii): this should probably be a hard failure.
2608 print('WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
2609 % r)
2610 for c in sorted(cc):
2611 # refspec option will be rejected if cc doesn't correspond to an
2612 # account, even though REST call to add such arbitrary cc may succeed.
2613 if c in valid_accounts:
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002614 refspec_opts.append('cc=%s' % c)
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002615 cc.remove(c)
2616
rmistry9eadede2016-09-19 11:22:43 -07002617 if options.topic:
2618 # Documentation on Gerrit topics is here:
2619 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002620 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002621
Edward Lemur687ca902018-12-05 02:30:30 +00002622 if options.enable_auto_submit:
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002623 refspec_opts.append('l=Auto-Submit+1')
Edward Lesmes10c3dd62021-02-08 21:13:57 +00002624 if options.set_bot_commit:
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002625 refspec_opts.append('l=Bot-Commit+1')
Edward Lemur687ca902018-12-05 02:30:30 +00002626 if options.use_commit_queue:
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002627 refspec_opts.append('l=Commit-Queue+2')
Edward Lemur687ca902018-12-05 02:30:30 +00002628 elif options.cq_dry_run:
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002629 refspec_opts.append('l=Commit-Queue+1')
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00002630 elif options.cq_quick_run:
2631 refspec_opts.append('l=Commit-Queue+1')
2632 refspec_opts.append('l=Quick-Run+1')
Edward Lemur687ca902018-12-05 02:30:30 +00002633
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002634 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08002635 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002636 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08002637 hashtags.update(change_desc.get_hash_tags())
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002638 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
2639
2640 refspec_suffix = ''
2641 if refspec_opts:
2642 refspec_suffix = '%' + ','.join(refspec_opts)
2643 assert ' ' not in refspec_suffix, (
2644 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2645 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002646
Edward Lemur1b52d872019-05-09 21:12:12 +00002647 git_push_metadata = {
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002648 'gerrit_host': self.GetGerritHost(),
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002649 'title': options.title or '<untitled>',
Edward Lemur1b52d872019-05-09 21:12:12 +00002650 'change_id': change_id,
2651 'description': change_desc.description,
2652 }
Josip Sokcevicf2cfd3d2021-03-30 18:39:18 +00002653
Gavin Mak4e5e3992022-11-14 22:40:12 +00002654 # Gerrit may or may not update fast enough to return the correct patchset
2655 # number after we push. Get the pre-upload patchset and increment later.
2656 latest_ps = self.GetMostRecentPatchset(update=False) or 0
2657
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002658 push_stdout = self._RunGitPushWithTraces(refspec, refspec_opts,
Josip Sokcevicf2cfd3d2021-03-30 18:39:18 +00002659 git_push_metadata,
2660 options.push_options)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002661
2662 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07002663 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002664 change_numbers = [m.group(1)
2665 for m in map(regex.match, push_stdout.splitlines())
2666 if m]
2667 if len(change_numbers) != 1:
2668 DieWithError(
2669 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002670 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002671 self.SetIssue(change_numbers[0])
Gavin Mak4e5e3992022-11-14 22:40:12 +00002672 self.SetPatchset(latest_ps + 1)
Gavin Makbe2e9262022-11-08 23:41:55 +00002673 self._GitSetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY, ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002674
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002675 if self.GetIssue() and (reviewers or cc):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002676 # GetIssue() is not set in case of non-squash uploads according to tests.
Aaron Gable6e7ddb62020-05-27 22:23:29 +00002677 # TODO(crbug.com/751901): non-squash uploads in git cl should be removed.
Thiago Perrottab0fb8d52022-08-30 21:26:19 +00002678 gerrit_util.AddReviewers(self.GetGerritHost(),
2679 self._GerritChangeIdentifier(),
2680 reviewers,
2681 cc,
2682 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002683
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002684 return 0
2685
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002686 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
2687 change_desc):
2688 """Computes parent of the generated commit to be uploaded to Gerrit.
2689
2690 Returns revision or a ref name.
2691 """
2692 if custom_cl_base:
2693 # Try to avoid creating additional unintended CLs when uploading, unless
2694 # user wants to take this risk.
2695 local_ref_of_target_remote = self.GetRemoteBranch()[1]
2696 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
2697 local_ref_of_target_remote])
2698 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002699 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002700 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
2701 'If you proceed with upload, more than 1 CL may be created by '
2702 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
2703 'If you are certain that specified base `%s` has already been '
2704 'uploaded to Gerrit as another CL, you may proceed.\n' %
2705 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
2706 if not force:
2707 confirm_or_exit(
2708 'Do you take responsibility for cleaning up potential mess '
2709 'resulting from proceeding with upload?',
2710 action='upload')
2711 return custom_cl_base
2712
Aaron Gablef97e33d2017-03-30 15:44:27 -07002713 if remote != '.':
2714 return self.GetCommonAncestorWithUpstream()
2715
2716 # If our upstream branch is local, we base our squashed commit on its
2717 # squashed version.
2718 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2719
Aaron Gablef97e33d2017-03-30 15:44:27 -07002720 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07002721 return self.GetCommonAncestorWithUpstream()
Glen Robertson7d98e222020-08-27 17:53:11 +00002722 if upstream_branch_name == 'main':
2723 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07002724
2725 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002726 # TODO(tandrii): consider checking parent change in Gerrit and using its
2727 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
2728 # the tree hash of the parent branch. The upside is less likely bogus
2729 # requests to reupload parent change just because it's uploadhash is
2730 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Gavin Makbe2e9262022-11-08 23:41:55 +00002731 parent = scm.GIT.GetBranchConfig(settings.GetRoot(), upstream_branch_name,
2732 GERRIT_SQUASH_HASH_CONFIG_KEY)
Aaron Gablef97e33d2017-03-30 15:44:27 -07002733 # Verify that the upstream branch has been uploaded too, otherwise
2734 # Gerrit will create additional CLs when uploading.
2735 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2736 RunGitSilent(['rev-parse', parent + ':'])):
2737 DieWithError(
2738 '\nUpload upstream branch %s first.\n'
2739 'It is likely that this branch has been rebased since its last '
2740 'upload, so you just need to upload it again.\n'
2741 '(If you uploaded it with --no-squash, then branch dependencies '
2742 'are not supported, and you should reupload with --squash.)'
2743 % upstream_branch_name,
2744 change_desc)
2745 return parent
2746
Gavin Mak4e5e3992022-11-14 22:40:12 +00002747 def _UpdateWithExternalChanges(self):
2748 """Updates workspace with external changes.
2749
2750 Returns the commit hash that should be used as the merge base on upload.
2751 """
2752 local_ps = self.GetPatchset()
2753 if local_ps is None:
2754 return
2755
2756 external_ps = self.GetMostRecentPatchset(update=False)
Gavin Makf35a9eb2022-11-17 18:34:36 +00002757 if external_ps is None or local_ps == external_ps or \
2758 not self._IsPatchsetRangeSignificant(local_ps + 1, external_ps):
Gavin Mak4e5e3992022-11-14 22:40:12 +00002759 return
2760
2761 num_changes = external_ps - local_ps
Gavin Mak6f905472023-01-06 21:01:36 +00002762 if num_changes > 1:
2763 change_words = 'changes were'
2764 else:
2765 change_words = 'change was'
2766 print('\n%d external %s published to %s:\n' %
2767 (num_changes, change_words, self.GetIssueURL(short=True)))
2768
2769 # Print an overview of external changes.
2770 ps_to_commit = {}
2771 ps_to_info = {}
2772 revisions = self._GetChangeDetail(['ALL_REVISIONS'])
2773 for commit_id, revision_info in revisions.get('revisions', {}).items():
2774 ps_num = revision_info['_number']
2775 ps_to_commit[ps_num] = commit_id
2776 ps_to_info[ps_num] = revision_info
2777
2778 for ps in range(external_ps, local_ps, -1):
2779 commit = ps_to_commit[ps][:8]
2780 desc = ps_to_info[ps].get('description', '')
2781 print('Patchset %d [%s] %s' % (ps, commit, desc))
2782
2783 if not ask_for_explicit_yes('\nUploading as-is will override them. '
2784 'Get the latest changes and apply?'):
Gavin Mak4e5e3992022-11-14 22:40:12 +00002785 return
2786
2787 # Get latest Gerrit merge base. Use the first parent even if multiple exist.
2788 external_parent = self._GetChangeCommit(revision=external_ps)['parents'][0]
2789 external_base = external_parent['commit']
2790
2791 branch = git_common.current_branch()
2792 local_base = self.GetCommonAncestorWithUpstream()
2793 if local_base != external_base:
2794 print('\nLocal merge base %s is different from Gerrit %s.\n' %
2795 (local_base, external_base))
2796 if git_common.upstream(branch):
2797 DieWithError('Upstream branch set. Consider using `git rebase-update` '
2798 'to make these the same.')
2799 print('No upstream branch set. Consider setting it and using '
2800 '`git rebase-update`.\nContinuing upload with Gerrit merge base.')
2801
2802 # Fetch Gerrit's CL base if it doesn't exist locally.
2803 remote, _ = self.GetRemoteBranch()
2804 if not scm.GIT.IsValidRevision(settings.GetRoot(), external_base):
2805 RunGitSilent(['fetch', remote, external_base])
2806
2807 # Get the diff between local_ps and external_ps.
2808 issue = self.GetIssue()
Gavin Mak591ebaf2022-12-06 18:05:07 +00002809 changes_ref = 'refs/changes/%02d/%d/' % (issue % 100, issue)
Gavin Mak4e5e3992022-11-14 22:40:12 +00002810 RunGitSilent(['fetch', remote, changes_ref + str(local_ps)])
2811 last_uploaded = RunGitSilent(['rev-parse', 'FETCH_HEAD']).strip()
2812 RunGitSilent(['fetch', remote, changes_ref + str(external_ps)])
2813 latest_external = RunGitSilent(['rev-parse', 'FETCH_HEAD']).strip()
2814 diff = RunGitSilent(['diff', '%s..%s' % (last_uploaded, latest_external)])
2815
2816 # Diff can be empty in the case of trivial rebases.
2817 if not diff:
2818 return external_base
2819
2820 # Apply the diff.
2821 with gclient_utils.temporary_file() as diff_tempfile:
2822 gclient_utils.FileWrite(diff_tempfile, diff)
2823 clean_patch = RunGitWithCode(['apply', '--check', diff_tempfile])[0] == 0
2824 RunGitSilent(['apply', '-3', '--intent-to-add', diff_tempfile])
2825 if not clean_patch:
2826 # Normally patchset is set after upload. But because we exit, that never
2827 # happens. Updating here makes sure that subsequent uploads don't need
2828 # to fetch/apply the same diff again.
2829 self.SetPatchset(external_ps)
2830 DieWithError('\nPatch did not apply cleanly. Please resolve any '
2831 'conflicts and reupload.')
2832
2833 message = 'Incorporate external changes from '
2834 if num_changes == 1:
2835 message += 'patchset %d' % external_ps
2836 else:
2837 message += 'patchsets %d to %d' % (local_ps + 1, external_ps)
2838 RunGitSilent(['commit', '-am', message])
2839 # TODO(crbug.com/1382528): Use the previous commit's message as a default
2840 # patchset title instead of this 'Incorporate' message.
2841 return external_base
2842
Edward Lemura12175c2020-03-09 16:58:26 +00002843 def _AddChangeIdToCommitMessage(self, log_desc, args):
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002844 """Re-commits using the current message, assumes the commit hook is in
2845 place.
2846 """
Edward Lemura12175c2020-03-09 16:58:26 +00002847 RunGit(['commit', '--amend', '-m', log_desc])
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002848 new_log_desc = _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002849 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002850 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002851 return new_log_desc
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00002852
2853 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002854
tandriie113dfd2016-10-11 10:20:12 -07002855 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002856 try:
2857 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002858 except GerritChangeNotExists:
2859 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002860
2861 if data['status'] in ('ABANDONED', 'MERGED'):
2862 return 'CL %s is closed' % self.GetIssue()
2863
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002864 def GetGerritChange(self, patchset=None):
2865 """Returns a buildbucket.v2.GerritChange message for the current issue."""
Edward Lemur79d4f992019-11-11 23:49:02 +00002866 host = urllib.parse.urlparse(self.GetCodereviewServer()).hostname
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002867 issue = self.GetIssue()
Edward Lemur2c210a42019-09-16 23:58:35 +00002868 patchset = int(patchset or self.GetPatchset())
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002869 data = self._GetChangeDetail(['ALL_REVISIONS'])
2870
2871 assert host and issue and patchset, 'CL must be uploaded first'
2872
2873 has_patchset = any(
2874 int(revision_data['_number']) == patchset
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002875 for revision_data in data['revisions'].values())
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002876 if not has_patchset:
Aaron Gablea45ee112016-11-22 15:14:38 -08002877 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002878 (patchset, self.GetIssue()))
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002879
tandrii8c5a3532016-11-04 07:52:02 -07002880 return {
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002881 'host': host,
2882 'change': issue,
2883 'project': data['project'],
2884 'patchset': patchset,
tandrii8c5a3532016-11-04 07:52:02 -07002885 }
tandriie113dfd2016-10-11 10:20:12 -07002886
tandriide281ae2016-10-12 06:02:30 -07002887 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002888 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002889
Edward Lemur707d70b2018-02-07 00:50:14 +01002890 def GetReviewers(self):
2891 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
Mohamed Heikal171c0742018-11-09 20:38:51 +00002892 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01002893
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002894
Lei Zhang8a0efc12020-08-05 19:58:45 +00002895def _get_bug_line_values(default_project_prefix, bugs):
2896 """Given default_project_prefix and comma separated list of bugs, yields bug
2897 line values.
tandriif9aefb72016-07-01 09:06:51 -07002898
2899 Each bug can be either:
Lei Zhang8a0efc12020-08-05 19:58:45 +00002900 * a number, which is combined with default_project_prefix
tandriif9aefb72016-07-01 09:06:51 -07002901 * string, which is left as is.
2902
2903 This function may produce more than one line, because bugdroid expects one
2904 project per line.
2905
Lei Zhang8a0efc12020-08-05 19:58:45 +00002906 >>> list(_get_bug_line_values('v8:', '123,chromium:789'))
tandriif9aefb72016-07-01 09:06:51 -07002907 ['v8:123', 'chromium:789']
2908 """
2909 default_bugs = []
2910 others = []
2911 for bug in bugs.split(','):
2912 bug = bug.strip()
2913 if bug:
2914 try:
2915 default_bugs.append(int(bug))
2916 except ValueError:
2917 others.append(bug)
2918
2919 if default_bugs:
2920 default_bugs = ','.join(map(str, default_bugs))
Lei Zhang8a0efc12020-08-05 19:58:45 +00002921 if default_project_prefix:
2922 if not default_project_prefix.endswith(':'):
2923 default_project_prefix += ':'
2924 yield '%s%s' % (default_project_prefix, default_bugs)
tandriif9aefb72016-07-01 09:06:51 -07002925 else:
2926 yield default_bugs
2927 for other in sorted(others):
2928 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2929 yield other
2930
2931
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002932class ChangeDescription(object):
2933 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002934 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07002935 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07002936 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Dan Beamd8b04ca2019-10-10 21:23:26 +00002937 FIXED_LINE = r'^[ \t]*Fixed[ \t]*:[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01002938 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08002939 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
2940 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
Anthony Polito02b5af32019-12-02 19:49:47 +00002941 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):($|[^:])'
Nodir Turakulov23b82142017-11-16 11:04:25 -08002942 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002943
Dan Beamd8b04ca2019-10-10 21:23:26 +00002944 def __init__(self, description, bug=None, fixed=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002945 self._description_lines = (description or '').strip().splitlines()
Anthony Polito8b955342019-09-24 19:01:36 +00002946 if bug:
2947 regexp = re.compile(self.BUG_LINE)
2948 prefix = settings.GetBugPrefix()
2949 if not any((regexp.match(line) for line in self._description_lines)):
2950 values = list(_get_bug_line_values(prefix, bug))
2951 self.append_footer('Bug: %s' % ', '.join(values))
Dan Beamd8b04ca2019-10-10 21:23:26 +00002952 if fixed:
2953 regexp = re.compile(self.FIXED_LINE)
2954 prefix = settings.GetBugPrefix()
2955 if not any((regexp.match(line) for line in self._description_lines)):
2956 values = list(_get_bug_line_values(prefix, fixed))
2957 self.append_footer('Fixed: %s' % ', '.join(values))
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002958
agable@chromium.org42c20792013-09-12 17:34:49 +00002959 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08002960 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00002961 return '\n'.join(self._description_lines)
2962
2963 def set_description(self, desc):
2964 if isinstance(desc, basestring):
2965 lines = desc.splitlines()
2966 else:
2967 lines = [line.rstrip() for line in desc]
2968 while lines and not lines[0]:
2969 lines.pop(0)
2970 while lines and not lines[-1]:
2971 lines.pop(-1)
2972 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002973
Edward Lemur5a644f82020-03-18 16:44:57 +00002974 def ensure_change_id(self, change_id):
2975 description = self.description
2976 footer_change_ids = git_footers.get_footer_change_id(description)
2977 # Make sure that the Change-Id in the description matches the given one.
2978 if footer_change_ids != [change_id]:
2979 if footer_change_ids:
2980 # Remove any existing Change-Id footers since they don't match the
2981 # expected change_id footer.
2982 description = git_footers.remove_footer(description, 'Change-Id')
2983 print('WARNING: Change-Id has been set to %s. Use `git cl issue 0` '
2984 'if you want to set a new one.')
2985 # Add the expected Change-Id footer.
2986 description = git_footers.add_footer_change_id(description, change_id)
2987 self.set_description(description)
2988
Joanna Wang39811b12023-01-20 23:09:48 +00002989 def update_reviewers(self, reviewers):
2990 """Rewrites the R= line(s) as a single line each.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002991
2992 Args:
2993 reviewers (list(str)) - list of additional emails to use for reviewers.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002994 """
Joanna Wang39811b12023-01-20 23:09:48 +00002995 if not reviewers:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002996 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002997
2998 reviewers = set(reviewers)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002999
Joanna Wang39811b12023-01-20 23:09:48 +00003000 # Get the set of R= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003001 regexp = re.compile(self.R_LINE)
3002 matches = [regexp.match(line) for line in self._description_lines]
3003 new_desc = [l for i, l in enumerate(self._description_lines)
3004 if not matches[i]]
3005 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003006
Joanna Wang39811b12023-01-20 23:09:48 +00003007 # Construct new unified R= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003008
Joanna Wang39811b12023-01-20 23:09:48 +00003009 # First, update reviewers with names from the R= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003010 for match in matches:
3011 if not match:
3012 continue
Joanna Wang39811b12023-01-20 23:09:48 +00003013 reviewers.update(cleanup_list([match.group(2).strip()]))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003014
Joanna Wang39811b12023-01-20 23:09:48 +00003015 new_r_line = 'R=' + ', '.join(sorted(reviewers))
agable@chromium.org42c20792013-09-12 17:34:49 +00003016
3017 # Put the new lines in the description where the old first R= line was.
3018 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3019 if 0 <= line_loc < len(self._description_lines):
Joanna Wang39811b12023-01-20 23:09:48 +00003020 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003021 else:
Joanna Wang39811b12023-01-20 23:09:48 +00003022 self.append_footer(new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003023
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00003024 def set_preserve_tryjobs(self):
3025 """Ensures description footer contains 'Cq-Do-Not-Cancel-Tryjobs: true'."""
3026 footers = git_footers.parse_footers(self.description)
3027 for v in footers.get('Cq-Do-Not-Cancel-Tryjobs', []):
3028 if v.lower() == 'true':
3029 return
3030 self.append_footer('Cq-Do-Not-Cancel-Tryjobs: true')
3031
Anthony Polito8b955342019-09-24 19:01:36 +00003032 def prompt(self):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003033 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003034 self.set_description([
3035 '# Enter a description of the change.',
3036 '# This will be displayed on the codereview site.',
3037 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003038 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003039 '--------------------',
3040 ] + self._description_lines)
Dan Beamd8b04ca2019-10-10 21:23:26 +00003041 bug_regexp = re.compile(self.BUG_LINE)
3042 fixed_regexp = re.compile(self.FIXED_LINE)
Jonas Termansend0f79112019-03-22 15:28:26 +00003043 prefix = settings.GetBugPrefix()
Sigurd Schneider8630bb12020-11-11 14:02:49 +00003044 has_issue = lambda l: bug_regexp.match(l) or fixed_regexp.match(l)
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00003045
Dan Beamd8b04ca2019-10-10 21:23:26 +00003046 if not any((has_issue(line) for line in self._description_lines)):
Anthony Polito8b955342019-09-24 19:01:36 +00003047 self.append_footer('Bug: %s' % prefix)
tandriif9aefb72016-07-01 09:06:51 -07003048
Bruce Dawsonfc487042020-10-27 19:11:37 +00003049 print('Waiting for editor...')
agable@chromium.org42c20792013-09-12 17:34:49 +00003050 content = gclient_utils.RunEditor(self.description, True,
Edward Lemur79d4f992019-11-11 23:49:02 +00003051 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003052 if not content:
3053 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003054 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003055
Bruce Dawson2377b012018-01-11 16:46:49 -08003056 # Strip off comments and default inserted "Bug:" line.
3057 clean_lines = [line.rstrip() for line in lines if not
Jonas Termansend0f79112019-03-22 15:28:26 +00003058 (line.startswith('#') or
3059 line.rstrip() == "Bug:" or
3060 line.rstrip() == "Bug: " + prefix)]
agable@chromium.org42c20792013-09-12 17:34:49 +00003061 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003062 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003063 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003064
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003065 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003066 """Adds a footer line to the description.
3067
3068 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3069 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3070 that Gerrit footers are always at the end.
3071 """
3072 parsed_footer_line = git_footers.parse_footer(line)
3073 if parsed_footer_line:
3074 # Line is a gerrit footer in the form: Footer-Key: any value.
3075 # Thus, must be appended observing Gerrit footer rules.
3076 self.set_description(
3077 git_footers.add_footer(self.description,
3078 key=parsed_footer_line[0],
3079 value=parsed_footer_line[1]))
3080 return
3081
3082 if not self._description_lines:
3083 self._description_lines.append(line)
3084 return
3085
3086 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3087 if gerrit_footers:
3088 # git_footers.split_footers ensures that there is an empty line before
3089 # actual (gerrit) footers, if any. We have to keep it that way.
3090 assert top_lines and top_lines[-1] == ''
3091 top_lines, separator = top_lines[:-1], top_lines[-1:]
3092 else:
3093 separator = [] # No need for separator if there are no gerrit_footers.
3094
3095 prev_line = top_lines[-1] if top_lines else ''
3096 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3097 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3098 top_lines.append('')
3099 top_lines.append(line)
3100 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003101
tandrii99a72f22016-08-17 14:33:24 -07003102 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003103 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003104 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003105 reviewers = [match.group(2).strip()
3106 for match in matches
3107 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003108 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003109
bradnelsond975b302016-10-23 12:20:23 -07003110 def get_cced(self):
3111 """Retrieves the list of reviewers."""
3112 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3113 cced = [match.group(2).strip() for match in matches if match]
3114 return cleanup_list(cced)
3115
Nodir Turakulov23b82142017-11-16 11:04:25 -08003116 def get_hash_tags(self):
3117 """Extracts and sanitizes a list of Gerrit hashtags."""
3118 subject = (self._description_lines or ('',))[0]
3119 subject = re.sub(
3120 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3121
3122 tags = []
3123 start = 0
3124 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3125 while True:
3126 m = bracket_exp.match(subject, start)
3127 if not m:
3128 break
3129 tags.append(self.sanitize_hash_tag(m.group(1)))
3130 start = m.end()
3131
3132 if not tags:
3133 # Try "Tag: " prefix.
3134 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3135 if m:
3136 tags.append(self.sanitize_hash_tag(m.group(1)))
3137 return tags
3138
3139 @classmethod
3140 def sanitize_hash_tag(cls, tag):
3141 """Returns a sanitized Gerrit hash tag.
3142
3143 A sanitized hashtag can be used as a git push refspec parameter value.
3144 """
3145 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3146
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003147
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003148def FindCodereviewSettingsFile(filename='codereview.settings'):
3149 """Finds the given file starting in the cwd and going up.
3150
3151 Only looks up to the top of the repository unless an
3152 'inherit-review-settings-ok' file exists in the root of the repository.
3153 """
3154 inherit_ok_file = 'inherit-review-settings-ok'
3155 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003156 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003157 if os.path.isfile(os.path.join(root, inherit_ok_file)):
Aleksey Khoroshilov2a229712022-06-02 16:24:11 +00003158 root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003159 while True:
Aleksey Khoroshilov2a229712022-06-02 16:24:11 +00003160 if os.path.isfile(os.path.join(cwd, filename)):
3161 return open(os.path.join(cwd, filename))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003162 if cwd == root:
3163 break
Aleksey Khoroshilov2a229712022-06-02 16:24:11 +00003164 parent_dir = os.path.dirname(cwd)
3165 if parent_dir == cwd:
3166 # We hit the system root directory.
3167 break
3168 cwd = parent_dir
3169 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003170
3171
3172def LoadCodereviewSettingsFromFile(fileobj):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003173 """Parses a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003174 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003175
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003176 def SetProperty(name, setting, unset_error_ok=False):
3177 fullname = 'rietveld.' + name
3178 if setting in keyvals:
3179 RunGit(['config', fullname, keyvals[setting]])
3180 else:
3181 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3182
tandrii48df5812016-10-17 03:55:37 -07003183 if not keyvals.get('GERRIT_HOST', False):
3184 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003185 # Only server setting is required. Other settings can be absent.
3186 # In that case, we ignore errors raised during option deletion attempt.
Joanna Wangc8f23e22023-01-19 21:18:10 +00003187 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003188 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3189 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003190 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003191 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3192 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003193 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3194 unset_error_ok=True)
Jamie Madilldc4d19e2019-10-24 21:50:02 +00003195 SetProperty(
3196 'format-full-by-default', 'FORMAT_FULL_BY_DEFAULT', unset_error_ok=True)
Dirk Pranke6f0df682021-06-25 00:42:33 +00003197 SetProperty('use-python3', 'USE_PYTHON3', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003198
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003199 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003200 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003201
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003202 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
Edward Lesmes4de54132020-05-05 19:41:33 +00003203 RunGit(['config', 'gerrit.squash-uploads',
3204 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003205
tandrii@chromium.org28253532016-04-14 13:46:56 +00003206 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003207 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003208 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3209
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003210 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003211 # should be of the form
3212 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3213 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003214 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3215 keyvals['ORIGIN_URL_CONFIG']])
3216
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003217
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003218def urlretrieve(source, destination):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003219 """Downloads a network object to a local file, like urllib.urlretrieve.
3220
3221 This is necessary because urllib is broken for SSL connections via a proxy.
3222 """
Vadim Shtayuraf7b8f8f2021-11-15 19:10:05 +00003223 with open(destination, 'wb') as f:
Edward Lemur79d4f992019-11-11 23:49:02 +00003224 f.write(urllib.request.urlopen(source).read())
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003225
3226
ukai@chromium.org712d6102013-11-27 00:52:58 +00003227def hasSheBang(fname):
3228 """Checks fname is a #! script."""
3229 with open(fname) as f:
3230 return f.read(2).startswith('#!')
3231
3232
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003233def DownloadGerritHook(force):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003234 """Downloads and installs a Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003235
3236 Args:
3237 force: True to update hooks. False to install hooks if not present.
3238 """
ukai@chromium.org712d6102013-11-27 00:52:58 +00003239 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003240 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3241 if not os.access(dst, os.X_OK):
3242 if os.path.exists(dst):
3243 if not force:
3244 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003245 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003246 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003247 if not hasSheBang(dst):
3248 DieWithError('Not a script: %s\n'
3249 'You need to download from\n%s\n'
3250 'into .git/hooks/commit-msg and '
3251 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003252 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3253 except Exception:
3254 if os.path.exists(dst):
3255 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003256 DieWithError('\nFailed to download hooks.\n'
3257 'You need to download from\n%s\n'
3258 'into .git/hooks/commit-msg and '
3259 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003260
3261
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003262class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003263 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003264
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003265 def __init__(self):
3266 # Cached list of [host, identity, source], where source is either
3267 # .gitcookies or .netrc.
3268 self._all_hosts = None
3269
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003270 def ensure_configured_gitcookies(self):
3271 """Runs checks and suggests fixes to make git use .gitcookies from default
3272 path."""
3273 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3274 configured_path = RunGitSilent(
3275 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003276 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003277 if configured_path:
3278 self._ensure_default_gitcookies_path(configured_path, default)
3279 else:
3280 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003281
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003282 @staticmethod
3283 def _ensure_default_gitcookies_path(configured_path, default_path):
3284 assert configured_path
3285 if configured_path == default_path:
3286 print('git is already configured to use your .gitcookies from %s' %
3287 configured_path)
3288 return
3289
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003290 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003291 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3292 (configured_path, default_path))
3293
3294 if not os.path.exists(configured_path):
3295 print('However, your configured .gitcookies file is missing.')
3296 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3297 action='reconfigure')
3298 RunGit(['config', '--global', 'http.cookiefile', default_path])
3299 return
3300
3301 if os.path.exists(default_path):
3302 print('WARNING: default .gitcookies file already exists %s' %
3303 default_path)
3304 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3305 default_path)
3306
3307 confirm_or_exit('Move existing .gitcookies to default location?',
3308 action='move')
3309 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003310 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003311 print('Moved and reconfigured git to use .gitcookies from %s' %
3312 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003313
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003314 @staticmethod
3315 def _configure_gitcookies_path(default_path):
3316 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3317 if os.path.exists(netrc_path):
3318 print('You seem to be using outdated .netrc for git credentials: %s' %
3319 netrc_path)
3320 print('This tool will guide you through setting up recommended '
3321 '.gitcookies store for git credentials.\n'
3322 '\n'
3323 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3324 ' git config --global --unset http.cookiefile\n'
3325 ' mv %s %s.backup\n\n' % (default_path, default_path))
3326 confirm_or_exit(action='setup .gitcookies')
3327 RunGit(['config', '--global', 'http.cookiefile', default_path])
3328 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003329
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003330 def get_hosts_with_creds(self, include_netrc=False):
3331 if self._all_hosts is None:
3332 a = gerrit_util.CookiesAuthenticator()
Garrett Beaty08bb5c42022-09-21 17:34:20 +00003333 self._all_hosts = [(h, u, s) for h, u, s in itertools.chain((
3334 (h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.items()), (
3335 (h, u, '.gitcookies') for h, (u, _) in a.gitcookies.items()))
3336 if h.endswith(_GOOGLESOURCE)]
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003337
3338 if include_netrc:
3339 return self._all_hosts
3340 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3341
3342 def print_current_creds(self, include_netrc=False):
3343 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3344 if not hosts:
3345 print('No Git/Gerrit credentials found')
3346 return
Edward Lemur79d4f992019-11-11 23:49:02 +00003347 lengths = [max(map(len, (row[i] for row in hosts))) for i in range(3)]
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003348 header = [('Host', 'User', 'Which file'),
3349 ['=' * l for l in lengths]]
3350 for row in (header + hosts):
3351 print('\t'.join((('%%+%ds' % l) % s)
3352 for l, s in zip(lengths, row)))
3353
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003354 @staticmethod
3355 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003356 """Parses identity "git-<username>.domain" into <username> and domain."""
3357 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003358 # distinguishable from sub-domains. But we do know typical domains:
3359 if identity.endswith('.chromium.org'):
3360 domain = 'chromium.org'
3361 username = identity[:-len('.chromium.org')]
3362 else:
3363 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003364 if username.startswith('git-'):
3365 username = username[len('git-'):]
3366 return username, domain
3367
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003368 def has_generic_host(self):
3369 """Returns whether generic .googlesource.com has been configured.
3370
3371 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3372 """
3373 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
Garrett Beaty08bb5c42022-09-21 17:34:20 +00003374 if host == '.' + _GOOGLESOURCE:
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003375 return True
3376 return False
3377
3378 def _get_git_gerrit_identity_pairs(self):
3379 """Returns map from canonic host to pair of identities (Git, Gerrit).
3380
3381 One of identities might be None, meaning not configured.
3382 """
3383 host_to_identity_pairs = {}
3384 for host, identity, _ in self.get_hosts_with_creds():
Garrett Beaty08bb5c42022-09-21 17:34:20 +00003385 canonical = _canonical_git_googlesource_host(host)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003386 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3387 idx = 0 if canonical == host else 1
3388 pair[idx] = identity
3389 return host_to_identity_pairs
3390
3391 def get_partially_configured_hosts(self):
3392 return set(
Garrett Beaty08bb5c42022-09-21 17:34:20 +00003393 (host if i1 else _canonical_gerrit_googlesource_host(host))
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003394 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().items()
Garrett Beaty08bb5c42022-09-21 17:34:20 +00003395 if None in (i1, i2) and host != '.' + _GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003396
3397 def get_conflicting_hosts(self):
3398 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003399 host
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003400 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().items()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003401 if None not in (i1, i2) and i1 != i2)
3402
3403 def get_duplicated_hosts(self):
3404 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003405 return set(host for host, count in counters.items() if count > 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003406
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003407
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003408 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003409 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003410 hosts = sorted(hosts)
3411 assert hosts
3412 if extra_column_func is None:
3413 extras = [''] * len(hosts)
3414 else:
3415 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003416 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3417 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003418 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003419 lines.append(tmpl % he)
3420 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003421
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003422 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003423 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003424 yield ('.googlesource.com wildcard record detected',
3425 ['Chrome Infrastructure team recommends to list full host names '
3426 'explicitly.'],
3427 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003428
3429 dups = self.get_duplicated_hosts()
3430 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003431 yield ('The following hosts were defined twice',
3432 self._format_hosts(dups),
3433 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003434
3435 partial = self.get_partially_configured_hosts()
3436 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003437 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3438 'These hosts are missing',
Garrett Beaty08bb5c42022-09-21 17:34:20 +00003439 self._format_hosts(
3440 partial, lambda host: 'but %s defined' % _get_counterpart_host(
3441 host)), partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003442
3443 conflicting = self.get_conflicting_hosts()
3444 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003445 yield ('The following Git hosts have differing credentials from their '
3446 'Gerrit counterparts',
3447 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3448 tuple(self._get_git_gerrit_identity_pairs()[host])),
3449 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003450
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003451 def find_and_report_problems(self):
3452 """Returns True if there was at least one problem, else False."""
3453 found = False
3454 bad_hosts = set()
3455 for title, sublines, hosts in self._find_problems():
3456 if not found:
3457 found = True
3458 print('\n\n.gitcookies problem report:\n')
3459 bad_hosts.update(hosts or [])
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003460 print(' %s%s' % (title, (':' if sublines else '')))
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003461 if sublines:
3462 print()
3463 print(' %s' % '\n '.join(sublines))
3464 print()
3465
3466 if bad_hosts:
3467 assert found
3468 print(' You can manually remove corresponding lines in your %s file and '
3469 'visit the following URLs with correct account to generate '
3470 'correct credential lines:\n' %
3471 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
Garrett Beaty08bb5c42022-09-21 17:34:20 +00003472 print(' %s' % '\n '.join(
3473 sorted(
3474 set(gerrit_util.CookiesAuthenticator().get_new_password_url(
3475 _canonical_git_googlesource_host(host))
3476 for host in bad_hosts))))
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003477 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003478
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003479
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003480@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003481def CMDcreds_check(parser, args):
3482 """Checks credentials and suggests changes."""
3483 _, _ = parser.parse_args(args)
3484
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003485 # Code below checks .gitcookies. Abort if using something else.
3486 authn = gerrit_util.Authenticator.get()
3487 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
Edward Lemur57d47422020-03-06 20:43:07 +00003488 message = (
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003489 'This command is not designed for bot environment. It checks '
3490 '~/.gitcookies file not generally used on bots.')
Edward Lemur57d47422020-03-06 20:43:07 +00003491 # TODO(crbug.com/1059384): Automatically detect when running on cloudtop.
3492 if isinstance(authn, gerrit_util.GceAuthenticator):
3493 message += (
3494 '\n'
3495 'If you need to run this on GCE or a cloudtop instance, '
3496 'export SKIP_GCE_AUTH_FOR_GIT=1 in your env.')
3497 DieWithError(message)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003498
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003499 checker = _GitCookiesChecker()
3500 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003501
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003502 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003503 checker.print_current_creds(include_netrc=True)
3504
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003505 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003506 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003507 return 0
3508 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003509
3510
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003511@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003512def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003513 """Gets or sets base-url for this branch."""
Thiago Perrotta16d08f02022-07-20 18:18:50 +00003514 _, args = parser.parse_args(args)
Edward Lesmes50da7702020-03-30 19:23:43 +00003515 branchref = scm.GIT.GetBranchRef(settings.GetRoot())
Edward Lemur85153282020-02-14 22:06:29 +00003516 branch = scm.GIT.ShortBranchName(branchref)
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003517 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003518 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003519 return RunGit(['config', 'branch.%s.base-url' % branch],
3520 error_ok=False).strip()
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00003521
3522 print('Setting base-url to %s' % args[0])
3523 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3524 error_ok=False).strip()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003525
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003526
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003527def color_for_status(status):
3528 """Maps a Changelist status to color, for CMDstatus and other tools."""
Bruce Dawsonb73f8a92020-03-27 22:03:08 +00003529 BOLD = '\033[1m'
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003530 return {
Bruce Dawsonb73f8a92020-03-27 22:03:08 +00003531 'unsent': BOLD + Fore.YELLOW,
3532 'waiting': BOLD + Fore.RED,
3533 'reply': BOLD + Fore.YELLOW,
3534 'not lgtm': BOLD + Fore.RED,
3535 'lgtm': BOLD + Fore.GREEN,
3536 'commit': BOLD + Fore.MAGENTA,
3537 'closed': BOLD + Fore.CYAN,
3538 'error': BOLD + Fore.WHITE,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003539 }.get(status, Fore.WHITE)
3540
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003541
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003542def get_cl_statuses(changes, fine_grained, max_processes=None):
3543 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003544
3545 If fine_grained is true, this will fetch CL statuses from the server.
3546 Otherwise, simply indicate if there's a matching url for the given branches.
3547
3548 If max_processes is specified, it is used as the maximum number of processes
3549 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3550 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003551
3552 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003553 """
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003554 if not changes:
Edward Lemur61bf4172020-02-24 23:22:37 +00003555 return
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003556
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003557 if not fine_grained:
3558 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07003559 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003560 for cl in changes:
3561 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003562 return
3563
3564 # First, sort out authentication issues.
3565 logging.debug('ensuring credentials exist')
3566 for cl in changes:
3567 cl.EnsureAuthenticated(force=False, refresh=True)
3568
3569 def fetch(cl):
3570 try:
3571 return (cl, cl.GetStatus())
3572 except:
3573 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07003574 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003575 raise
3576
3577 threads_count = len(changes)
3578 if max_processes:
3579 threads_count = max(1, min(threads_count, max_processes))
3580 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3581
Edward Lemur61bf4172020-02-24 23:22:37 +00003582 pool = multiprocessing.pool.ThreadPool(threads_count)
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003583 fetched_cls = set()
3584 try:
3585 it = pool.imap_unordered(fetch, changes).__iter__()
3586 while True:
3587 try:
3588 cl, status = it.next(timeout=5)
Edward Lemur61bf4172020-02-24 23:22:37 +00003589 except (multiprocessing.TimeoutError, StopIteration):
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003590 break
3591 fetched_cls.add(cl)
3592 yield cl, status
3593 finally:
3594 pool.close()
3595
3596 # Add any branches that failed to fetch.
3597 for cl in set(changes) - fetched_cls:
3598 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003599
rmistry@google.com2dd99862015-06-22 12:22:18 +00003600
Jose Lopes3863fc52020-04-07 17:00:25 +00003601def upload_branch_deps(cl, args, force=False):
rmistry@google.com2dd99862015-06-22 12:22:18 +00003602 """Uploads CLs of local branches that are dependents of the current branch.
3603
3604 If the local branch dependency tree looks like:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003605
3606 test1 -> test2.1 -> test3.1
3607 -> test3.2
3608 -> test2.2 -> test3.3
rmistry@google.com2dd99862015-06-22 12:22:18 +00003609
3610 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3611 run on the dependent branches in this order:
3612 test2.1, test3.1, test3.2, test2.2, test3.3
3613
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003614 Note: This function does not rebase your local dependent branches. Use it
3615 when you make a change to the parent branch that will not conflict
3616 with its dependent branches, and you would like their dependencies
3617 updated in Rietveld.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003618 """
3619 if git_common.is_dirty_git_tree('upload-branch-deps'):
3620 return 1
3621
3622 root_branch = cl.GetBranch()
3623 if root_branch is None:
3624 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3625 'Get on a branch!')
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00003626 if not cl.GetIssue():
rmistry@google.com2dd99862015-06-22 12:22:18 +00003627 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3628 'patchset dependencies without an uploaded CL.')
3629
3630 branches = RunGit(['for-each-ref',
3631 '--format=%(refname:short) %(upstream:short)',
3632 'refs/heads'])
3633 if not branches:
3634 print('No local branches found.')
3635 return 0
3636
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003637 # Create a dictionary of all local branches to the branches that are
3638 # dependent on it.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003639 tracked_to_dependents = collections.defaultdict(list)
3640 for b in branches.splitlines():
3641 tokens = b.split()
3642 if len(tokens) == 2:
3643 branch_name, tracked = tokens
3644 tracked_to_dependents[tracked].append(branch_name)
3645
vapiera7fbd5a2016-06-16 09:17:49 -07003646 print()
3647 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003648 dependents = []
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003649
rmistry@google.com2dd99862015-06-22 12:22:18 +00003650 def traverse_dependents_preorder(branch, padding=''):
3651 dependents_to_process = tracked_to_dependents.get(branch, [])
3652 padding += ' '
3653 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003654 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003655 dependents.append(dependent)
3656 traverse_dependents_preorder(dependent, padding)
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003657
rmistry@google.com2dd99862015-06-22 12:22:18 +00003658 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003659 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003660
3661 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003662 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003663 return 0
3664
Jose Lopes3863fc52020-04-07 17:00:25 +00003665 if not force:
3666 confirm_or_exit('This command will checkout all dependent branches and run '
3667 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003668
rmistry@google.com2dd99862015-06-22 12:22:18 +00003669 # Record all dependents that failed to upload.
3670 failures = {}
3671 # Go through all dependents, checkout the branch and upload.
3672 try:
3673 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003674 print()
3675 print('--------------------------------------')
3676 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003677 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003678 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003679 try:
3680 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003681 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003682 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003683 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003684 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003685 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003686 finally:
3687 # Swap back to the original root branch.
3688 RunGit(['checkout', '-q', root_branch])
3689
vapiera7fbd5a2016-06-16 09:17:49 -07003690 print()
3691 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003692 for dependent_branch in dependents:
3693 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003694 print(' %s : %s' % (dependent_branch, upload_status))
3695 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003696
3697 return 0
3698
3699
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00003700def GetArchiveTagForBranch(issue_num, branch_name, existing_tags, pattern):
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003701 """Given a proposed tag name, returns a tag name that is guaranteed to be
3702 unique. If 'foo' is proposed but already exists, then 'foo-2' is used,
3703 or 'foo-3', and so on."""
3704
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00003705 proposed_tag = pattern.format(**{'issue': issue_num, 'branch': branch_name})
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003706 for suffix_num in itertools.count(1):
3707 if suffix_num == 1:
3708 to_check = proposed_tag
3709 else:
3710 to_check = '%s-%d' % (proposed_tag, suffix_num)
3711
3712 if to_check not in existing_tags:
3713 return to_check
3714
3715
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003716@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07003717def CMDarchive(parser, args):
3718 """Archives and deletes branches associated with closed changelists."""
3719 parser.add_option(
3720 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003721 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003722 parser.add_option(
3723 '-f', '--force', action='store_true',
3724 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003725 parser.add_option(
3726 '-d', '--dry-run', action='store_true',
3727 help='Skip the branch tagging and removal steps.')
3728 parser.add_option(
3729 '-t', '--notags', action='store_true',
3730 help='Do not tag archived branches. '
3731 'Note: local commit history may be lost.')
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00003732 parser.add_option(
3733 '-p',
3734 '--pattern',
3735 default='git-cl-archived-{issue}-{branch}',
3736 help='Format string for archive tags. '
3737 'E.g. \'archived-{issue}-{branch}\'.')
kmarshall3bff56b2016-06-06 18:31:47 -07003738
kmarshall3bff56b2016-06-06 18:31:47 -07003739 options, args = parser.parse_args(args)
3740 if args:
3741 parser.error('Unsupported args: %s' % ' '.join(args))
kmarshall3bff56b2016-06-06 18:31:47 -07003742
3743 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3744 if not branches:
3745 return 0
3746
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003747 tags = RunGit(['for-each-ref', '--format=%(refname)',
3748 'refs/tags']).splitlines() or []
3749 tags = [t.split('/')[-1] for t in tags]
3750
vapiera7fbd5a2016-06-16 09:17:49 -07003751 print('Finding all branches associated with closed issues...')
Edward Lemur934836a2019-09-09 20:16:54 +00003752 changes = [Changelist(branchref=b)
3753 for b in branches.splitlines()]
kmarshall3bff56b2016-06-06 18:31:47 -07003754 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3755 statuses = get_cl_statuses(changes,
3756 fine_grained=True,
3757 max_processes=options.maxjobs)
3758 proposal = [(cl.GetBranch(),
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00003759 GetArchiveTagForBranch(cl.GetIssue(), cl.GetBranch(), tags,
3760 options.pattern))
kmarshall3bff56b2016-06-06 18:31:47 -07003761 for cl, status in statuses
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00003762 if status in ('closed', 'rietveld-not-supported')]
kmarshall3bff56b2016-06-06 18:31:47 -07003763 proposal.sort()
3764
3765 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003766 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003767 return 0
3768
Edward Lemur85153282020-02-14 22:06:29 +00003769 current_branch = scm.GIT.GetBranch(settings.GetRoot())
kmarshall3bff56b2016-06-06 18:31:47 -07003770
vapiera7fbd5a2016-06-16 09:17:49 -07003771 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003772 if options.notags:
3773 for next_item in proposal:
3774 print(' ' + next_item[0])
3775 else:
3776 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3777 for next_item in proposal:
3778 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003779
kmarshall9249e012016-08-23 12:02:16 -07003780 # Quit now on precondition failure or if instructed by the user, either
3781 # via an interactive prompt or by command line flags.
3782 if options.dry_run:
3783 print('\nNo changes were made (dry run).\n')
3784 return 0
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00003785
3786 if any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003787 print('You are currently on a branch \'%s\' which is associated with a '
3788 'closed codereview issue, so archive cannot proceed. Please '
3789 'checkout another branch and run this command again.' %
3790 current_branch)
3791 return 1
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00003792
3793 if not options.force:
Edward Lesmesae3586b2020-03-23 21:21:14 +00003794 answer = gclient_utils.AskForData('\nProceed with deletion (Y/n)? ').lower()
sergiyb4a5ecbe2016-06-20 09:46:00 -07003795 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003796 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003797 return 1
3798
3799 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003800 if not options.notags:
3801 RunGit(['tag', tagname, branch])
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003802
3803 if RunGitWithCode(['branch', '-D', branch])[0] != 0:
3804 # Clean up the tag if we failed to delete the branch.
3805 RunGit(['tag', '-d', tagname])
kmarshall9249e012016-08-23 12:02:16 -07003806
vapiera7fbd5a2016-06-16 09:17:49 -07003807 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003808
3809 return 0
3810
3811
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003812@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003813def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003814 """Show status of changelists.
3815
3816 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003817 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07003818 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003819 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07003820 - Red 'not LGTM'ed
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00003821 - Magenta in the CQ
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003822 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07003823 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003824
3825 Also see 'git cl comments'.
3826 """
Alan Cuttera3be9a52019-03-04 18:50:33 +00003827 parser.add_option(
3828 '--no-branch-color',
3829 action='store_true',
3830 help='Disable colorized branch names')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003831 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003832 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003833 parser.add_option('-f', '--fast', action='store_true',
3834 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003835 parser.add_option(
3836 '-j', '--maxjobs', action='store', type=int,
3837 help='The maximum number of jobs to use when retrieving review status')
Edward Lemur52969c92020-02-06 18:15:28 +00003838 parser.add_option(
3839 '-i', '--issue', type=int,
3840 help='Operate on this issue instead of the current branch\'s implicit '
3841 'issue. Requires --field to be set.')
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00003842 parser.add_option('-d',
3843 '--date-order',
3844 action='store_true',
3845 help='Order branches by committer date.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003846 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003847 if args:
3848 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003849
iannuccie53c9352016-08-17 14:40:40 -07003850 if options.issue is not None and not options.field:
Edward Lemur6c6827c2020-02-06 21:15:18 +00003851 parser.error('--field must be given when --issue is set.')
iannucci3c972b92016-08-17 13:24:10 -07003852
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003853 if options.field:
Edward Lemur934836a2019-09-09 20:16:54 +00003854 cl = Changelist(issue=options.issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003855 if options.field.startswith('desc'):
Edward Lemur6c6827c2020-02-06 21:15:18 +00003856 if cl.GetIssue():
3857 print(cl.FetchDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003858 elif options.field == 'id':
3859 issueid = cl.GetIssue()
3860 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003861 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003862 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08003863 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003864 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003865 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003866 elif options.field == 'status':
3867 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003868 elif options.field == 'url':
3869 url = cl.GetIssueURL()
3870 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003871 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003872 return 0
3873
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00003874 branches = RunGit([
3875 'for-each-ref', '--format=%(refname) %(committerdate:unix)', 'refs/heads'
3876 ])
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003877 if not branches:
3878 print('No local branch found.')
3879 return 0
3880
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003881 changes = [
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00003882 Changelist(branchref=b, commit_date=ct)
3883 for b, ct in map(lambda line: line.split(' '), branches.splitlines())
3884 ]
vapiera7fbd5a2016-06-16 09:17:49 -07003885 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003886 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003887 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003888 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003889
Edward Lemur85153282020-02-14 22:06:29 +00003890 current_branch = scm.GIT.GetBranch(settings.GetRoot())
Daniel McArdlea23bf592019-02-12 00:25:12 +00003891
3892 def FormatBranchName(branch, colorize=False):
3893 """Simulates 'git branch' behavior. Colorizes and prefixes branch name with
3894 an asterisk when it is the current branch."""
3895
3896 asterisk = ""
3897 color = Fore.RESET
3898 if branch == current_branch:
3899 asterisk = "* "
3900 color = Fore.GREEN
Edward Lemur85153282020-02-14 22:06:29 +00003901 branch_name = scm.GIT.ShortBranchName(branch)
Daniel McArdlea23bf592019-02-12 00:25:12 +00003902
3903 if colorize:
3904 return asterisk + color + branch_name + Fore.RESET
Daniel McArdle452a49f2019-02-14 17:28:31 +00003905 return asterisk + branch_name
3906
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003907 branch_statuses = {}
Daniel McArdlea23bf592019-02-12 00:25:12 +00003908
3909 alignment = max(5, max(len(FormatBranchName(c.GetBranch())) for c in changes))
Sigurd Schneider1bfda8e2021-06-30 14:46:25 +00003910
3911 if options.date_order or settings.IsStatusCommitOrderByDate():
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00003912 sorted_changes = sorted(changes,
3913 key=lambda c: c.GetCommitDate(),
3914 reverse=True)
3915 else:
3916 sorted_changes = sorted(changes, key=lambda c: c.GetBranch())
3917 for cl in sorted_changes:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003918 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003919 while branch not in branch_statuses:
Edward Lemur79d4f992019-11-11 23:49:02 +00003920 c, status = next(output)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003921 branch_statuses[c.GetBranch()] = status
3922 status = branch_statuses.pop(branch)
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +00003923 url = cl.GetIssueURL(short=True)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003924 if url and (not status or status == 'error'):
3925 # The issue probably doesn't exist anymore.
3926 url += ' (broken)'
3927
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003928 color = color_for_status(status)
Bruce Dawsonb73f8a92020-03-27 22:03:08 +00003929 # Turn off bold as well as colors.
3930 END = '\033[0m'
3931 reset = Fore.RESET + END
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003932 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003933 color = ''
3934 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003935 status_str = '(%s)' % status if status else ''
Daniel McArdle452a49f2019-02-14 17:28:31 +00003936
Alan Cuttera3be9a52019-03-04 18:50:33 +00003937 branch_display = FormatBranchName(branch)
3938 padding = ' ' * (alignment - len(branch_display))
3939 if not options.no_branch_color:
3940 branch_display = FormatBranchName(branch, colorize=True)
Daniel McArdle452a49f2019-02-14 17:28:31 +00003941
Alan Cuttera3be9a52019-03-04 18:50:33 +00003942 print(' %s : %s%s %s%s' % (padding + branch_display, color, url,
3943 status_str, reset))
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003944
vapiera7fbd5a2016-06-16 09:17:49 -07003945 print()
Daniel McArdlea23bf592019-02-12 00:25:12 +00003946 print('Current branch: %s' % current_branch)
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003947 for cl in changes:
Daniel McArdlea23bf592019-02-12 00:25:12 +00003948 if cl.GetBranch() == current_branch:
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003949 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003950 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003951 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003952 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003953 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003954 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003955 print('Issue description:')
Edward Lemur6c6827c2020-02-06 21:15:18 +00003956 print(cl.FetchDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003957 return 0
3958
3959
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003960def colorize_CMDstatus_doc():
3961 """To be called once in main() to add colors to git cl status help."""
3962 colors = [i for i in dir(Fore) if i[0].isupper()]
3963
3964 def colorize_line(line):
3965 for color in colors:
3966 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003967 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003968 indent = len(line) - len(line.lstrip(' ')) + 1
3969 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3970 return line
3971
3972 lines = CMDstatus.__doc__.splitlines()
3973 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3974
3975
phajdan.jre328cf92016-08-22 04:12:17 -07003976def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07003977 if path == '-':
3978 json.dump(contents, sys.stdout)
3979 else:
3980 with open(path, 'w') as f:
3981 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07003982
3983
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003984@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003985@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003986def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003987 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003988
3989 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003990 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003991 parser.add_option('-r', '--reverse', action='store_true',
3992 help='Lookup the branch(es) for the specified issues. If '
3993 'no issues are specified, all branches with mapped '
3994 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07003995 parser.add_option('--json',
3996 help='Path to JSON output file, or "-" for stdout.')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003997 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003998
dnj@chromium.org406c4402015-03-03 17:22:28 +00003999 if options.reverse:
4000 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004001 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004002 # Reverse issue lookup.
4003 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004004
4005 git_config = {}
4006 for config in RunGit(['config', '--get-regexp',
4007 r'branch\..*issue']).splitlines():
4008 name, _space, val = config.partition(' ')
4009 git_config[name] = val
4010
dnj@chromium.org406c4402015-03-03 17:22:28 +00004011 for branch in branches:
Edward Lesmes50da7702020-03-30 19:23:43 +00004012 issue = git_config.get(
4013 'branch.%s.%s' % (scm.GIT.ShortBranchName(branch), ISSUE_CONFIG_KEY))
Edward Lemur52969c92020-02-06 18:15:28 +00004014 if issue:
4015 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004016 if not args:
Carlos Caballero81923d62020-07-06 18:22:27 +00004017 args = sorted(issue_branch_map.keys())
phajdan.jre328cf92016-08-22 04:12:17 -07004018 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004019 for issue in args:
Lei Zhang5a368d42019-03-25 23:18:19 +00004020 try:
4021 issue_num = int(issue)
4022 except ValueError:
4023 print('ERROR cannot parse issue number: %s' % issue, file=sys.stderr)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004024 continue
Lei Zhang5a368d42019-03-25 23:18:19 +00004025 result[issue_num] = issue_branch_map.get(issue_num)
vapiera7fbd5a2016-06-16 09:17:49 -07004026 print('Branch for issue number %s: %s' % (
Lei Zhang5a368d42019-03-25 23:18:19 +00004027 issue, ', '.join(issue_branch_map.get(issue_num) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004028 if options.json:
4029 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004030 return 0
4031
4032 if len(args) > 0:
Edward Lemurf38bc172019-09-03 21:02:13 +00004033 issue = ParseIssueNumberArgument(args[0])
Aaron Gable78753da2017-06-15 10:35:49 -07004034 if not issue.valid:
4035 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4036 'or no argument to list it.\n'
4037 'Maybe you want to run git cl status?')
Edward Lemurf38bc172019-09-03 21:02:13 +00004038 cl = Changelist()
Aaron Gable78753da2017-06-15 10:35:49 -07004039 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004040 else:
Edward Lemurf38bc172019-09-03 21:02:13 +00004041 cl = Changelist()
Aaron Gable78753da2017-06-15 10:35:49 -07004042 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4043 if options.json:
4044 write_json(options.json, {
Nodir Turakulov27379632021-03-17 18:53:29 +00004045 'gerrit_host': cl.GetGerritHost(),
4046 'gerrit_project': cl.GetGerritProject(),
Aaron Gable78753da2017-06-15 10:35:49 -07004047 'issue_url': cl.GetIssueURL(),
Nodir Turakulov27379632021-03-17 18:53:29 +00004048 'issue': cl.GetIssue(),
Aaron Gable78753da2017-06-15 10:35:49 -07004049 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004050 return 0
4051
4052
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004053@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004054def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004055 """Shows or posts review comments for any changelist."""
4056 parser.add_option('-a', '--add-comment', dest='comment',
4057 help='comment to add to an issue')
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004058 parser.add_option('-p', '--publish', action='store_true',
4059 help='marks CL as ready and sends comment to reviewers')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004060 parser.add_option('-i', '--issue', dest='issue',
Edward Lemurf38bc172019-09-03 21:02:13 +00004061 help='review issue id (defaults to current issue).')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004062 parser.add_option('-m', '--machine-readable', dest='readable',
4063 action='store_false', default=True,
4064 help='output comments in a format compatible with '
4065 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004066 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004067 help='File to write JSON summary to, or "-" for stdout')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004068 options, args = parser.parse_args(args)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004069
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004070 issue = None
4071 if options.issue:
4072 try:
4073 issue = int(options.issue)
4074 except ValueError:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004075 DieWithError('A review issue ID is expected to be a number.')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004076
Edward Lemur934836a2019-09-09 20:16:54 +00004077 cl = Changelist(issue=issue)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004078
4079 if options.comment:
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004080 cl.AddComment(options.comment, options.publish)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004081 return 0
4082
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004083 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4084 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004085 for comment in summary:
4086 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004087 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004088 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004089 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004090 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004091 color = Fore.MAGENTA
Quinten Yearsley0e617c02019-02-20 00:37:03 +00004092 elif comment.autogenerated:
4093 color = Fore.CYAN
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004094 else:
4095 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004096 print('\n%s%s %s%s\n%s' % (
4097 color,
4098 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4099 comment.sender,
4100 Fore.RESET,
4101 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4102
smut@google.comc85ac942015-09-15 16:34:43 +00004103 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004104 def pre_serialize(c):
Edward Lemur79d4f992019-11-11 23:49:02 +00004105 dct = c._asdict().copy()
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004106 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4107 return dct
Edward Lemur79d4f992019-11-11 23:49:02 +00004108 write_json(options.json_file, [pre_serialize(x) for x in summary])
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004109 return 0
4110
4111
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004112@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004113@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004114def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004115 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004116 parser.add_option('-d', '--display', action='store_true',
4117 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004118 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004119 help='New description to set for this issue (- for stdin, '
4120 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004121 parser.add_option('-f', '--force', action='store_true',
4122 help='Delete any unpublished Gerrit edits for this issue '
4123 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004124
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004125 options, args = parser.parse_args(args)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004126
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004127 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004128 if len(args) > 0:
Edward Lemurf38bc172019-09-03 21:02:13 +00004129 target_issue_arg = ParseIssueNumberArgument(args[0])
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004130 if not target_issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004131 parser.error('Invalid issue ID or URL.')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004132
Edward Lemur934836a2019-09-09 20:16:54 +00004133 kwargs = {}
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004134 if target_issue_arg:
4135 kwargs['issue'] = target_issue_arg.issue
4136 kwargs['codereview_host'] = target_issue_arg.hostname
martiniss6eda05f2016-06-30 10:18:35 -07004137
4138 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004139 if not cl.GetIssue():
4140 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004141
Edward Lemur678a6842019-10-03 22:25:05 +00004142 if args and not args[0].isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +00004143 logging.info('canonical issue/change URL: %s\n', cl.GetIssueURL())
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004144
Edward Lemur6c6827c2020-02-06 21:15:18 +00004145 description = ChangeDescription(cl.FetchDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004146
smut@google.com34fb6b12015-07-13 20:03:26 +00004147 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004148 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004149 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004150
4151 if options.new_description:
4152 text = options.new_description
4153 if text == '-':
4154 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004155 elif text == '+':
4156 base_branch = cl.GetCommonAncestorWithUpstream()
Edward Lemura12175c2020-03-09 16:58:26 +00004157 text = _create_description_from_log([base_branch])
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004158
4159 description.set_description(text)
4160 else:
Edward Lemurf38bc172019-09-03 21:02:13 +00004161 description.prompt()
Edward Lemur6c6827c2020-02-06 21:15:18 +00004162 if cl.FetchDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004163 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004164 return 0
4165
4166
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004167@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004168def CMDlint(parser, args):
4169 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004170 parser.add_option('--filter', action='append', metavar='-x,+y',
4171 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004172 options, args = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004173
4174 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004175 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004176 try:
4177 import cpplint
4178 import cpplint_chromium
4179 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004180 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004181 return 1
4182
4183 # Change the current working directory before calling lint so that it
4184 # shows the correct base.
4185 previous_cwd = os.getcwd()
4186 os.chdir(settings.GetRoot())
4187 try:
Edward Lemur934836a2019-09-09 20:16:54 +00004188 cl = Changelist()
Edward Lemur2c62b332020-03-12 22:12:33 +00004189 files = cl.GetAffectedFiles(cl.GetCommonAncestorWithUpstream())
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004190 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004191 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004192 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004193
Lei Zhangb8c62cf2020-07-15 20:09:37 +00004194 # Process cpplint arguments, if any.
4195 filters = presubmit_canned_checks.GetCppLintFilters(options.filter)
4196 command = ['--filter=' + ','.join(filters)] + args + files
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004197 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004198
Lei Zhang379d1ad2020-07-15 19:40:06 +00004199 include_regex = re.compile(settings.GetLintRegex())
4200 ignore_regex = re.compile(settings.GetLintIgnoreRegex())
thestig@chromium.org44202a22014-03-11 19:22:18 +00004201 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4202 for filename in filenames:
Lei Zhang379d1ad2020-07-15 19:40:06 +00004203 if not include_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004204 print('Skipping file %s' % filename)
Lei Zhang379d1ad2020-07-15 19:40:06 +00004205 continue
4206
4207 if ignore_regex.match(filename):
4208 print('Ignoring file %s' % filename)
4209 continue
4210
4211 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4212 extra_check_functions)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004213 finally:
4214 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004215 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004216 if cpplint._cpplint_state.error_count != 0:
4217 return 1
4218 return 0
4219
4220
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004221@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004222def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004223 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004224 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004225 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004226 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004227 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004228 parser.add_option('--all', action='store_true',
4229 help='Run checks against all files, not just modified ones')
Josip Sokcevic017544d2022-03-31 23:47:53 +00004230 parser.add_option('--files',
4231 nargs=1,
4232 help='Semicolon-separated list of files to be marked as '
4233 'modified when executing presubmit or post-upload hooks. '
4234 'fnmatch wildcards can also be used.')
Edward Lesmes8e282792018-04-03 18:50:29 -04004235 parser.add_option('--parallel', action='store_true',
4236 help='Run all tests specified by input_api.RunTests in all '
4237 'PRESUBMIT files in parallel.')
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00004238 parser.add_option('--resultdb', action='store_true',
4239 help='Run presubmit checks in the ResultSink environment '
4240 'and send results to the ResultDB database.')
Saagar Sanghavi03b15132020-08-10 16:43:41 +00004241 parser.add_option('--realm', help='LUCI realm if reporting to ResultDB')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004242 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004243
sbc@chromium.org71437c02015-04-09 19:29:40 +00004244 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004245 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004246 return 1
4247
Edward Lemur934836a2019-09-09 20:16:54 +00004248 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004249 if args:
4250 base_branch = args[0]
4251 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004252 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004253 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004254
Bruce Dawsoneb8426e2022-08-05 23:58:15 +00004255 start = time.time()
4256 try:
4257 if not 'PRESUBMIT_SKIP_NETWORK' in os.environ and cl.GetIssue():
4258 description = cl.FetchDescription()
4259 else:
4260 description = _create_description_from_log([base_branch])
4261 except Exception as e:
4262 print('Failed to fetch CL description - %s' % str(e))
Edward Lemura12175c2020-03-09 16:58:26 +00004263 description = _create_description_from_log([base_branch])
Bruce Dawsoneb8426e2022-08-05 23:58:15 +00004264 elapsed = time.time() - start
4265 if elapsed > 5:
4266 print('%.1f s to get CL description.' % elapsed)
Aaron Gable8076c282017-11-29 14:39:41 -08004267
Bruce Dawson13acea32022-05-03 22:13:08 +00004268 if not base_branch:
4269 if not options.force:
4270 print('use --force to check even when not on a branch.')
4271 return 1
4272 base_branch = 'HEAD'
4273
Josip Sokcevic017544d2022-03-31 23:47:53 +00004274 cl.RunHook(committing=not options.upload,
4275 may_prompt=False,
4276 verbose=options.verbose,
4277 parallel=options.parallel,
4278 upstream=base_branch,
4279 description=description,
4280 all_files=options.all,
4281 files=options.files,
4282 resultdb=options.resultdb,
4283 realm=options.realm)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004284 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004285
4286
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004287def GenerateGerritChangeId(message):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004288 """Returns the Change ID footer value (Ixxxxxx...xxx).
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004289
4290 Works the same way as
4291 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4292 but can be called on demand on all platforms.
4293
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004294 The basic idea is to generate git hash of a state of the tree, original
4295 commit message, author/committer info and timestamps.
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004296 """
4297 lines = []
4298 tree_hash = RunGitSilent(['write-tree'])
4299 lines.append('tree %s' % tree_hash.strip())
4300 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4301 if code == 0:
4302 lines.append('parent %s' % parent.strip())
4303 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4304 lines.append('author %s' % author.strip())
4305 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4306 lines.append('committer %s' % committer.strip())
4307 lines.append('')
4308 # Note: Gerrit's commit-hook actually cleans message of some lines and
4309 # whitespace. This code is not doing this, but it clearly won't decrease
4310 # entropy.
4311 lines.append(message)
4312 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
Raul Tambreb946b232019-03-26 14:48:46 +00004313 stdin=('\n'.join(lines)).encode())
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004314 return 'I%s' % change_hash.strip()
4315
4316
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004317def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004318 """Computes the remote branch ref to use for the CL.
4319
4320 Args:
4321 remote (str): The git remote for the CL.
4322 remote_branch (str): The git remote branch for the CL.
4323 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004324 """
4325 if not (remote and remote_branch):
4326 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004327
wittman@chromium.org455dc922015-01-26 20:15:50 +00004328 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004329 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004330 # refs, which are then translated into the remote full symbolic refs
4331 # below.
4332 if '/' not in target_branch:
4333 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4334 else:
4335 prefix_replacements = (
4336 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4337 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4338 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4339 )
4340 match = None
4341 for regex, replacement in prefix_replacements:
4342 match = re.search(regex, target_branch)
4343 if match:
4344 remote_branch = target_branch.replace(match.group(0), replacement)
4345 break
4346 if not match:
4347 # This is a branch path but not one we recognize; use as-is.
4348 remote_branch = target_branch
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00004349 # pylint: disable=consider-using-get
rmistry@google.comc68112d2015-03-03 12:48:06 +00004350 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00004351 # pylint: enable=consider-using-get
rmistry@google.comc68112d2015-03-03 12:48:06 +00004352 # Handle the refs that need to land in different refs.
4353 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004354
wittman@chromium.org455dc922015-01-26 20:15:50 +00004355 # Create the true path to the remote branch.
4356 # Does the following translation:
4357 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
Josip Sokcevicc39ab992020-09-24 20:09:15 +00004358 # * refs/remotes/origin/main -> refs/heads/main
wittman@chromium.org455dc922015-01-26 20:15:50 +00004359 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4360 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4361 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4362 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4363 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4364 'refs/heads/')
4365 elif remote_branch.startswith('refs/remotes/branch-heads'):
4366 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004367
wittman@chromium.org455dc922015-01-26 20:15:50 +00004368 return remote_branch
4369
4370
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004371def cleanup_list(l):
4372 """Fixes a list so that comma separated items are put as individual items.
4373
4374 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4375 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4376 """
4377 items = sum((i.split(',') for i in l), [])
4378 stripped_items = (i.strip() for i in items)
4379 return sorted(filter(None, stripped_items))
4380
4381
Aaron Gable4db38df2017-11-03 14:59:07 -07004382@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004383@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004384def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004385 """Uploads the current changelist to codereview.
4386
4387 Can skip dependency patchset uploads for a branch by running:
4388 git config branch.branch_name.skip-deps-uploads True
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004389 To unset, run:
rmistry@google.com78948ed2015-07-08 23:09:57 +00004390 git config --unset branch.branch_name.skip-deps-uploads
4391 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004392
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004393 If the name of the checked out branch starts with "bug-" or "fix-" followed
4394 by a bug number, this bug number is automatically populated in the CL
Dominic Battre7d1c4842017-10-27 09:17:28 +02004395 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004396
4397 If subject contains text in square brackets or has "<text>: " prefix, such
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004398 text(s) is treated as Gerrit hashtags. For example, CLs with subjects:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004399 [git-cl] add support for hashtags
4400 Foo bar: implement foo
4401 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004402 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004403 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4404 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004405 parser.add_option('--bypass-watchlists', action='store_true',
4406 dest='bypass_watchlists',
4407 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004408 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004409 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004410 parser.add_option('--message', '-m', dest='message',
4411 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004412 parser.add_option('-b', '--bug',
4413 help='pre-populate the bug number(s) for this issue. '
4414 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004415 parser.add_option('--message-file', dest='message_file',
4416 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004417 parser.add_option('--title', '-t', dest='title',
4418 help='title for patchset')
Mario Bianuccicebfb4e2020-07-22 23:08:16 +00004419 parser.add_option('-T', '--skip-title', action='store_true',
4420 dest='skip_title',
4421 help='Use the most recent commit message as the title of '
4422 'the patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004423 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004424 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004425 help='reviewer email addresses')
4426 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004427 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004428 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004429 parser.add_option('--hashtag', dest='hashtags',
4430 action='append', default=[],
4431 help=('Gerrit hashtag for new CL; '
4432 'can be applied multiple times'))
Thiago Perrottab0fb8d52022-08-30 21:26:19 +00004433 parser.add_option('-s',
4434 '--send-mail',
4435 '--send-email',
4436 dest='send_mail',
4437 action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004438 help='send email to reviewer(s) and cc(s) immediately')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004439 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004440 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004441 metavar='TARGET',
4442 help='Apply CL to remote ref TARGET. ' +
Josip Sokcevicc39ab992020-09-24 20:09:15 +00004443 'Default: remote branch head, or main')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004444 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004445 help='Squash multiple commits into one')
Mike Frysingera989d552019-08-14 20:51:23 +00004446 parser.add_option('--no-squash', action='store_false', dest='squash',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004447 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004448 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004449 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004450 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4451 const='R', help='add a set of OWNERS to R')
Thiago Perrottab0fb8d52022-08-30 21:26:19 +00004452 parser.add_option('-c',
4453 '--use-commit-queue',
4454 action='store_true',
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004455 default=False,
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004456 help='tell the CQ to commit this patchset; '
Thiago Perrottab0fb8d52022-08-30 21:26:19 +00004457 'implies --send-mail')
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004458 parser.add_option('-d', '--cq-dry-run',
4459 action='store_true', default=False,
rmistry@google.comef966222015-04-07 11:15:01 +00004460 help='Send the patchset to do a CQ dry run right after '
4461 'upload.')
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00004462 parser.add_option(
4463 '-q',
4464 '--cq-quick-run',
4465 action='store_true',
4466 default=False,
4467 help='Send the patchset to do a CQ quick run right after '
4468 'upload (https://source.chromium.org/chromium/chromium/src/+/main:do'
4469 'cs/cq_quick_run.md) (chromium only).')
Edward Lesmes10c3dd62021-02-08 21:13:57 +00004470 parser.add_option('--set-bot-commit', action='store_true',
4471 help=optparse.SUPPRESS_HELP)
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00004472 parser.add_option('--preserve-tryjobs', action='store_true',
4473 help='instruct the CQ to let tryjobs running even after '
4474 'new patchsets are uploaded instead of canceling '
4475 'prior patchset\' tryjobs')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004476 parser.add_option('--dependencies', action='store_true',
4477 help='Uploads CLs of all the local branches that depend on '
4478 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04004479 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4480 help='Sends your change to the CQ after an approval. Only '
4481 'works on repos that have the Auto-Submit label '
4482 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04004483 parser.add_option('--parallel', action='store_true',
4484 help='Run all tests specified by input_api.RunTests in all '
4485 'PRESUBMIT files in parallel.')
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004486 parser.add_option('--no-autocc', action='store_true',
4487 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004488 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004489 help='Set the review private. This implies --no-autocc.')
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004490 parser.add_option('-R', '--retry-failed', action='store_true',
4491 help='Retry failed tryjobs from old patchset immediately '
4492 'after uploading new patchset. Cannot be used with '
4493 '--use-commit-queue or --cq-dry-run.')
4494 parser.add_option('--buildbucket-host', default='cr-buildbucket.appspot.com',
4495 help='Host of buildbucket. The default host is %default.')
Dan Beamd8b04ca2019-10-10 21:23:26 +00004496 parser.add_option('--fixed', '-x',
4497 help='List of bugs that will be commented on and marked '
4498 'fixed (pre-populates "Fixed:" tag). Same format as '
4499 '-b option / "Bug:" tag. If fixing several issues, '
4500 'separate with commas.')
Josipe827b0f2020-01-30 00:07:20 +00004501 parser.add_option('--edit-description', action='store_true', default=False,
4502 help='Modify description before upload. Cannot be used '
4503 'with --force. It is a noop when --no-squash is set '
4504 'or a new commit is created.')
Ng Zhi Ancdaf0be2020-05-27 20:57:28 +00004505 parser.add_option('--git-completion-helper', action="store_true",
4506 help=optparse.SUPPRESS_HELP)
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00004507 parser.add_option('--resultdb', action='store_true',
4508 help='Run presubmit checks in the ResultSink environment '
4509 'and send results to the ResultDB database.')
Saagar Sanghavi03b15132020-08-10 16:43:41 +00004510 parser.add_option('--realm', help='LUCI realm if reporting to ResultDB')
Josip Sokcevicf2cfd3d2021-03-30 18:39:18 +00004511 parser.add_option('-o',
4512 '--push-options',
4513 action='append',
4514 default=[],
4515 help='Transmit the given string to the server when '
4516 'performing git push (pass-through). See git-push '
4517 'documentation for more details.')
Gregory Nisbet48d9e1e2021-04-15 23:35:54 +00004518 parser.add_option('--no-add-changeid',
4519 action='store_true',
4520 dest='no_add_changeid',
4521 help='Do not add change-ids to messages.')
Brian Sheedy7326ca22022-11-02 18:36:17 +00004522 parser.add_option('--no-python2-post-upload-hooks',
4523 action='store_true',
4524 help='Only run post-upload hooks in Python 3.')
Joanna Wang18de1f62023-01-21 01:24:24 +00004525 parser.add_option('--stacked-exp',
4526 action='store_true',
4527 help=optparse.SUPPRESS_HELP)
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004528
rmistry@google.com2dd99862015-06-22 12:22:18 +00004529 orig_args = args
ukai@chromium.orge8077812012-02-03 03:41:46 +00004530 (options, args) = parser.parse_args(args)
4531
Ng Zhi Ancdaf0be2020-05-27 20:57:28 +00004532 if options.git_completion_helper:
Edward Lesmesb7db1832020-06-22 20:22:27 +00004533 print(' '.join(opt.get_opt_string() for opt in parser.option_list
4534 if opt.help != optparse.SUPPRESS_HELP))
4535 return
Ng Zhi Ancdaf0be2020-05-27 20:57:28 +00004536
sbc@chromium.org71437c02015-04-09 19:29:40 +00004537 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004538 return 1
4539
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004540 options.reviewers = cleanup_list(options.reviewers)
4541 options.cc = cleanup_list(options.cc)
4542
Josipe827b0f2020-01-30 00:07:20 +00004543 if options.edit_description and options.force:
4544 parser.error('Only one of --force and --edit-description allowed')
4545
tandriib80458a2016-06-23 12:20:07 -07004546 if options.message_file:
4547 if options.message:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004548 parser.error('Only one of --message and --message-file allowed.')
tandriib80458a2016-06-23 12:20:07 -07004549 options.message = gclient_utils.FileRead(options.message_file)
tandriib80458a2016-06-23 12:20:07 -07004550
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004551 if ([options.cq_dry_run,
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00004552 options.cq_quick_run,
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004553 options.use_commit_queue,
4554 options.retry_failed].count(True) > 1):
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00004555 parser.error('Only one of --use-commit-queue, --cq-dry-run, --cq-quick-run '
4556 'or --retry-failed is allowed.')
tandrii4d0545a2016-07-06 03:56:49 -07004557
Mario Bianuccicebfb4e2020-07-22 23:08:16 +00004558 if options.skip_title and options.title:
4559 parser.error('Only one of --title and --skip-title allowed.')
4560
Aaron Gableedbc4132017-09-11 13:22:28 -07004561 if options.use_commit_queue:
4562 options.send_mail = True
4563
Edward Lesmes0dd54822020-03-26 18:24:25 +00004564 if options.squash is None:
4565 # Load default for user, repo, squash=true, in this order.
4566 options.squash = settings.GetSquashGerritUploads()
4567
Joanna Wang18de1f62023-01-21 01:24:24 +00004568 if options.stacked_exp:
4569 orig_args.remove('--stacked-exp')
4570
4571 UploadAllSquashed(options, orig_args)
4572 return 0
4573
Josip Sokcevic1cabb172021-04-08 19:15:35 +00004574 cl = Changelist(branchref=options.target_branch)
Edward Lesmes7677e5c2020-02-19 20:39:03 +00004575 # Warm change details cache now to avoid RPCs later, reducing latency for
4576 # developers.
4577 if cl.GetIssue():
4578 cl._GetChangeDetail(
4579 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
4580
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004581 if options.retry_failed and not cl.GetIssue():
4582 print('No previous patchsets, so --retry-failed has no effect.')
4583 options.retry_failed = False
Edward Lesmes7677e5c2020-02-19 20:39:03 +00004584
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004585 # cl.GetMostRecentPatchset uses cached information, and can return the last
4586 # patchset before upload. Calling it here makes it clear that it's the
4587 # last patchset before upload. Note that GetMostRecentPatchset will fail
4588 # if no CL has been uploaded yet.
4589 if options.retry_failed:
4590 patchset = cl.GetMostRecentPatchset()
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004591
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004592 ret = cl.CMDUpload(options, args, orig_args)
4593
4594 if options.retry_failed:
4595 if ret != 0:
4596 print('Upload failed, so --retry-failed has no effect.')
4597 return ret
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +00004598 builds, _ = _fetch_latest_builds(
Edward Lemur5b929a42019-10-21 17:57:39 +00004599 cl, options.buildbucket_host, latest_patchset=patchset)
Edward Lemur45768512020-03-02 19:03:14 +00004600 jobs = _filter_failed_for_retry(builds)
4601 if len(jobs) == 0:
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004602 print('No failed tryjobs, so --retry-failed has no effect.')
4603 return ret
Quinten Yearsley777660f2020-03-04 23:37:06 +00004604 _trigger_tryjobs(cl, jobs, options, patchset + 1)
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004605
4606 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00004607
4608
Joanna Wang18de1f62023-01-21 01:24:24 +00004609def UploadAllSquashed(options, orig_args):
4610 # type: (optparse.Values, Sequence[str]) -> Tuple[Sequence[Changelist], bool]
4611 """Uploads the current and upstream branches (if necessary)."""
4612 _cls, _cherry_pick_current = _UploadAllPrecheck(options, orig_args)
4613
4614 # TODO(b/265929888): parse cls and create commits.
4615
4616
4617def _UploadAllPrecheck(options, orig_args):
4618 # type: (optparse.Values, Sequence[str]) -> Tuple[Sequence[Changelist], bool]
4619 """Checks the state of the tree and gives the user uploading options
4620
4621 Returns: A tuple of the ordered list of changes that have new commits
4622 since their last upload and a boolean of whether the user wants to
4623 cherry-pick and upload the current branch instead of uploading all cls.
4624 """
4625 branch_ref = None
4626 cls = []
4627 must_upload_upstream = False
4628
4629 Changelist._GerritCommitMsgHookCheck(offer_removal=not options.force)
4630
4631 while True:
4632 if len(cls) > _MAX_STACKED_BRANCHES_UPLOAD:
4633 DieWithError(
4634 'More than %s branches in the stack have not been uploaded.\n'
4635 'Are your branches in a misconfigured state?\n'
4636 'If not, please upload some upstream changes first.' %
4637 (_MAX_STACKED_BRANCHES_UPLOAD))
4638
4639 cl = Changelist(branchref=branch_ref)
4640 cls.append(cl)
4641
4642 origin, upstream_branch_ref = Changelist.FetchUpstreamTuple(cl.GetBranch())
4643 upstream_branch = scm.GIT.ShortBranchName(upstream_branch_ref)
4644 branch_ref = upstream_branch_ref # set branch for next run.
4645
4646 # Case 1: We've reached the beginning of the tree.
4647 if origin != '.':
4648 break
4649
4650 upstream_last_upload = scm.GIT.GetBranchConfig(settings.GetRoot(),
4651 upstream_branch,
4652 LAST_UPLOAD_HASH_CONFIG_KEY)
4653
4654 # Case 2: If any upstream branches have never been uploaded,
4655 # the user MUST upload them.
4656 if not upstream_last_upload:
4657 must_upload_upstream = True
4658 continue
4659
4660 base_commit = cl.GetCommonAncestorWithUpstream()
4661
4662 # Case 3: If upstream's last_upload == cl.base_commit we do
4663 # not need to upload any more upstreams from this point on.
4664 # (Even if there may be diverged branches higher up the tree)
4665 if base_commit == upstream_last_upload:
4666 break
4667
4668 # Case 4: If upstream's last_upload < cl.base_commit we are
4669 # uploading cl and upstream_cl.
4670 # Continue up the tree to check other branch relations.
Joanna Wangab9c6ba2023-01-21 01:46:36 +00004671 if scm.GIT.IsAncestor(upstream_last_upload, base_commit):
Joanna Wang18de1f62023-01-21 01:24:24 +00004672 continue
4673
4674 # Case 5: If cl.base_commit < upstream's last_upload the user
4675 # must rebase before uploading.
Joanna Wangab9c6ba2023-01-21 01:46:36 +00004676 if scm.GIT.IsAncestor(base_commit, upstream_last_upload):
Joanna Wang18de1f62023-01-21 01:24:24 +00004677 DieWithError(
4678 'At least one branch in the stack has diverged from its upstream '
4679 'branch and does not contain its upstream\'s last upload.\n'
4680 'Please rebase the stack with `git rebase-update` before uploading.')
4681
4682 # The tree went through a rebase. LAST_UPLOAD_HASH_CONFIG_KEY no longer has
4683 # any relation to commits in the tree. Continue up the tree until we hit
4684 # the root.
4685
4686 # We assume all cls in the stack have the same auth requirements and only
4687 # check this once.
4688 cls[0].EnsureAuthenticated(force=options.force)
4689
4690 cherry_pick = False
4691 if len(cls) > 1:
4692 message = ''
4693 if len(orig_args):
4694 message = ('options %s will be used for all uploads.\n' % orig_args)
4695 if must_upload_upstream:
4696 confirm_or_exit('\n' + message +
4697 'There are upstream branches that must be uploaded.\n')
4698 else:
4699 answer = gclient_utils.AskForData(
4700 '\n' + message +
4701 'Press enter to update branches %s.\nOr type `n` to upload only '
4702 '`%s` cherry-picked on %s\'s last upload:' %
4703 ([cl.branch for cl in cls], cls[0].branch, cls[1].branch))
4704 if answer.lower() == 'n':
4705 cherry_pick = True
4706 return cls, cherry_pick
4707
4708
Francois Dorayd42c6812017-05-30 15:10:20 -04004709@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004710@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04004711def CMDsplit(parser, args):
4712 """Splits a branch into smaller branches and uploads CLs.
4713
4714 Creates a branch and uploads a CL for each group of files modified in the
4715 current branch that share a common OWNERS file. In the CL description and
Edward Lemurac5c55f2020-02-29 00:17:16 +00004716 comment, the string '$directory', is replaced with the directory containing
4717 the shared OWNERS file.
Francois Dorayd42c6812017-05-30 15:10:20 -04004718 """
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004719 parser.add_option('-d', '--description', dest='description_file',
4720 help='A text file containing a CL description in which '
4721 '$directory will be replaced by each CL\'s directory.')
4722 parser.add_option('-c', '--comment', dest='comment_file',
4723 help='A text file containing a CL comment.')
4724 parser.add_option('-n', '--dry-run', dest='dry_run', action='store_true',
Chris Watkinsba28e462017-12-13 11:22:17 +11004725 default=False,
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004726 help='List the files and reviewers for each CL that would '
4727 'be created, but don\'t create branches or CLs.')
4728 parser.add_option('--cq-dry-run', action='store_true',
4729 help='If set, will do a cq dry run for each uploaded CL. '
4730 'Please be careful when doing this; more than ~10 CLs '
4731 'has the potential to overload our build '
4732 'infrastructure. Try to upload these not during high '
4733 'load times (usually 11-3 Mountain View time). Email '
4734 'infra-dev@chromium.org with any questions.')
Takuto Ikuta51eca592019-02-14 19:40:52 +00004735 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4736 default=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')
Daniel Cheng403c44e2022-10-05 22:24:58 +00004740 parser.add_option('--max-depth',
4741 type='int',
4742 default=0,
4743 help='The max depth to look for OWNERS files. Useful for '
4744 'controlling the granularity of the split CLs, e.g. '
4745 '--max-depth=1 will only split by top-level '
4746 'directory. Specifying a value less than 1 means no '
4747 'limit on max depth.')
Francois Dorayd42c6812017-05-30 15:10:20 -04004748 options, _ = parser.parse_args(args)
4749
4750 if not options.description_file:
4751 parser.error('No --description flag specified.')
4752
4753 def WrappedCMDupload(args):
4754 return CMDupload(OptionParser(), args)
4755
Daniel Cheng403c44e2022-10-05 22:24:58 +00004756 return split_cl.SplitCl(options.description_file, options.comment_file,
4757 Changelist, WrappedCMDupload, options.dry_run,
4758 options.cq_dry_run, options.enable_auto_submit,
4759 options.max_depth, settings.GetRoot())
Francois Dorayd42c6812017-05-30 15:10:20 -04004760
4761
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004762@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004763@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004764def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004765 """DEPRECATED: Used to commit the current changelist via git-svn."""
4766 message = ('git-cl no longer supports committing to SVN repositories via '
4767 'git-svn. You probably want to use `git cl land` instead.')
4768 print(message)
4769 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004770
4771
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004772@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004773@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004774def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004775 """Commits the current changelist via git.
4776
4777 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4778 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004779 """
4780 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4781 help='bypass upload presubmit hook')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004782 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004783 help="force yes to questions (don't prompt)")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04004784 parser.add_option('--parallel', action='store_true',
4785 help='Run all tests specified by input_api.RunTests in all '
4786 'PRESUBMIT files in parallel.')
Saagar Sanghavi03b15132020-08-10 16:43:41 +00004787 parser.add_option('--resultdb', action='store_true',
4788 help='Run presubmit checks in the ResultSink environment '
4789 'and send results to the ResultDB database.')
4790 parser.add_option('--realm', help='LUCI realm if reporting to ResultDB')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004791 (options, args) = parser.parse_args(args)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004792
Edward Lemur934836a2019-09-09 20:16:54 +00004793 cl = Changelist()
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004794
Robert Iannucci2e73d432018-03-14 01:10:47 -07004795 if not cl.GetIssue():
4796 DieWithError('You must upload the change first to Gerrit.\n'
4797 ' If you would rather have `git cl land` upload '
4798 'automatically for you, see http://crbug.com/642759')
Saagar Sanghavi03b15132020-08-10 16:43:41 +00004799 return cl.CMDLand(options.force, options.bypass_hooks, options.verbose,
4800 options.parallel, options.resultdb, options.realm)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004801
4802
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004803@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004804@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004805def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004806 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004807 parser.add_option('-b', dest='newbranch',
4808 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004809 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07004810 help='overwrite state on the current or chosen branch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004811 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
Edward Lemurf38bc172019-09-03 21:02:13 +00004812 help='don\'t commit after patch applies.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004813
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004814 group = optparse.OptionGroup(
4815 parser,
4816 'Options for continuing work on the current issue uploaded from a '
4817 'different clone (e.g. different machine). Must be used independently '
4818 'from the other options. No issue number should be specified, and the '
4819 'branch must have an issue number associated with it')
4820 group.add_option('--reapply', action='store_true', dest='reapply',
4821 help='Reset the branch and reapply the issue.\n'
4822 'CAUTION: This will undo any local changes in this '
4823 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004824
4825 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004826 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004827 parser.add_option_group(group)
4828
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004829 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004830
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004831 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004832 if options.newbranch:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004833 parser.error('--reapply works on the current branch only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004834 if len(args) > 0:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004835 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004836
Edward Lemur934836a2019-09-09 20:16:54 +00004837 cl = Changelist()
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004838 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004839 parser.error('Current branch must have an associated issue.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004840
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004841 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004842 if upstream is None:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004843 parser.error('No upstream branch specified. Cannot reset branch.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004844
4845 RunGit(['reset', '--hard', upstream])
4846 if options.pull:
4847 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004848
Edward Lemur678a6842019-10-03 22:25:05 +00004849 target_issue_arg = ParseIssueNumberArgument(cl.GetIssue())
Bruce Dawsonf362f6f2021-02-18 23:15:17 +00004850 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.nocommit, False,
4851 False)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004852
4853 if len(args) != 1 or not args[0]:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004854 parser.error('Must specify issue number or URL.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004855
Edward Lemurf38bc172019-09-03 21:02:13 +00004856 target_issue_arg = ParseIssueNumberArgument(args[0])
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004857 if not target_issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004858 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004859
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004860 # We don't want uncommitted changes mixed up with the patch.
4861 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004862 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004863
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004864 if options.newbranch:
4865 if options.force:
4866 RunGit(['branch', '-D', options.newbranch],
4867 stderr=subprocess2.PIPE, error_ok=True)
Edward Lemur84101642020-02-21 21:40:34 +00004868 git_new_branch.create_new_branch(options.newbranch)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004869
Edward Lemur678a6842019-10-03 22:25:05 +00004870 cl = Changelist(
4871 codereview_host=target_issue_arg.hostname, issue=target_issue_arg.issue)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004872
Edward Lemur678a6842019-10-03 22:25:05 +00004873 if not args[0].isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +00004874 print('canonical issue/change URL: %s\n' % cl.GetIssueURL())
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004875
Bruce Dawsonf362f6f2021-02-18 23:15:17 +00004876 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.nocommit,
4877 options.force, options.newbranch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004878
4879
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004880def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004881 """Fetches the tree status and returns either 'open', 'closed',
4882 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004883 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004884 if url:
Daniel McArdle8b4eeff2020-07-20 17:02:47 +00004885 status = str(urllib.request.urlopen(url).read().lower())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004886 if status.find('closed') != -1 or status == '0':
4887 return 'closed'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00004888
4889 if status.find('open') != -1 or status == '1':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004890 return 'open'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00004891
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004892 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004893 return 'unset'
4894
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004895
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004896def GetTreeStatusReason():
4897 """Fetches the tree status from a json url and returns the message
4898 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004899 url = settings.GetTreeStatusUrl()
Daniel McArdle8b4eeff2020-07-20 17:02:47 +00004900 json_url = urllib.parse.urljoin(url, '/current?format=json')
Edward Lemur79d4f992019-11-11 23:49:02 +00004901 connection = urllib.request.urlopen(json_url)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004902 status = json.loads(connection.read())
4903 connection.close()
4904 return status['message']
4905
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004906
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004907@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004908def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004909 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004910 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004911 status = GetTreeStatus()
4912 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004913 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004914 return 2
4915
vapiera7fbd5a2016-06-16 09:17:49 -07004916 print('The tree is %s' % status)
4917 print()
4918 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004919 if status != 'open':
4920 return 1
4921 return 0
4922
4923
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004924@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00004925def CMDtry(parser, args):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004926 """Triggers tryjobs using either Buildbucket or CQ dry run."""
4927 group = optparse.OptionGroup(parser, 'Tryjob options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004928 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004929 '-b', '--bot', action='append',
4930 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4931 'times to specify multiple builders. ex: '
4932 '"-b win_rel -b win_layout". See '
4933 'the try server waterfall for the builders name and the tests '
4934 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004935 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004936 '-B', '--bucket', default='',
Ben Pastene08a30b22022-05-04 17:46:38 +00004937 help=('Buildbucket bucket to send the try requests. Format: '
4938 '"luci.$LUCI_PROJECT.$LUCI_BUCKET". eg: "luci.chromium.try"'))
borenet6c0efe62016-10-19 08:13:29 -07004939 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004940 '-r', '--revision',
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004941 help='Revision to use for the tryjob; default: the revision will '
tandriif7b29d42016-10-07 08:45:41 -07004942 'be determined by the try recipe that builder runs, which usually '
Josip Sokcevicc39ab992020-09-24 20:09:15 +00004943 'defaults to HEAD of origin/master or origin/main')
maruel@chromium.org15192402012-09-06 12:38:29 +00004944 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004945 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004946 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004947 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004948 group.add_option(
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00004949 '-q',
4950 '--quick-run',
4951 action='store_true',
4952 default=False,
4953 help='trigger in quick run mode '
4954 '(https://source.chromium.org/chromium/chromium/src/+/main:docs/cq_q'
4955 'uick_run.md) (chromium only).')
4956 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08004957 '--category', default='git_cl_try', help='Specify custom build category.')
4958 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004959 '--project',
4960 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004961 'in recipe to determine to which repository or directory to '
4962 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004963 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004964 '-p', '--property', dest='properties', action='append', default=[],
4965 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004966 'key2=value2 etc. The value will be treated as '
4967 'json if decodable, or as string otherwise. '
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004968 'NOTE: using this may make your tryjob not usable for CQ, '
4969 'which will then schedule another tryjob with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004970 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004971 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4972 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004973 parser.add_option_group(group)
Quinten Yearsley983111f2019-09-26 17:18:48 +00004974 parser.add_option(
4975 '-R', '--retry-failed', action='store_true', default=False,
4976 help='Retry failed jobs from the latest set of tryjobs. '
4977 'Not allowed with --bucket and --bot options.')
Edward Lemur52969c92020-02-06 18:15:28 +00004978 parser.add_option(
4979 '-i', '--issue', type=int,
4980 help='Operate on this issue instead of the current branch\'s implicit '
4981 'issue.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004982 options, args = parser.parse_args(args)
4983
machenbach@chromium.org45453142015-09-15 08:45:22 +00004984 # Make sure that all properties are prop=value pairs.
4985 bad_params = [x for x in options.properties if '=' not in x]
4986 if bad_params:
4987 parser.error('Got properties with missing "=": %s' % bad_params)
4988
maruel@chromium.org15192402012-09-06 12:38:29 +00004989 if args:
4990 parser.error('Unknown arguments: %s' % args)
4991
Edward Lemur934836a2019-09-09 20:16:54 +00004992 cl = Changelist(issue=options.issue)
maruel@chromium.org15192402012-09-06 12:38:29 +00004993 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004994 parser.error('Need to upload first.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004995
Edward Lemurf38bc172019-09-03 21:02:13 +00004996 # HACK: warm up Gerrit change detail cache to save on RPCs.
Edward Lemur125d60a2019-09-13 18:25:41 +00004997 cl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01004998
tandriie113dfd2016-10-11 10:20:12 -07004999 error_message = cl.CannotTriggerTryJobReason()
5000 if error_message:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005001 parser.error('Can\'t trigger tryjobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005002
Edward Lemur45768512020-03-02 19:03:14 +00005003 if options.bot:
5004 if options.retry_failed:
5005 parser.error('--bot is not compatible with --retry-failed.')
5006 if not options.bucket:
5007 parser.error('A bucket (e.g. "chromium/try") is required.')
5008
5009 triggered = [b for b in options.bot if 'triggered' in b]
5010 if triggered:
5011 parser.error(
5012 'Cannot schedule builds on triggered bots: %s.\n'
5013 'This type of bot requires an initial job from a parent (usually a '
5014 'builder). Schedule a job on the parent instead.\n' % triggered)
5015
5016 if options.bucket.startswith('.master'):
5017 parser.error('Buildbot masters are not supported.')
5018
5019 project, bucket = _parse_bucket(options.bucket)
5020 if project is None or bucket is None:
5021 parser.error('Invalid bucket: %s.' % options.bucket)
5022 jobs = sorted((project, bucket, bot) for bot in options.bot)
5023 elif options.retry_failed:
Quinten Yearsley983111f2019-09-26 17:18:48 +00005024 print('Searching for failed tryjobs...')
Edward Lemur5b929a42019-10-21 17:57:39 +00005025 builds, patchset = _fetch_latest_builds(cl, options.buildbucket_host)
Quinten Yearsley983111f2019-09-26 17:18:48 +00005026 if options.verbose:
5027 print('Got %d builds in patchset #%d' % (len(builds), patchset))
Edward Lemur45768512020-03-02 19:03:14 +00005028 jobs = _filter_failed_for_retry(builds)
5029 if not jobs:
Quinten Yearsley983111f2019-09-26 17:18:48 +00005030 print('There are no failed jobs in the latest set of jobs '
5031 '(patchset #%d), doing nothing.' % patchset)
5032 return 0
Edward Lemur45768512020-03-02 19:03:14 +00005033 num_builders = len(jobs)
Quinten Yearsley983111f2019-09-26 17:18:48 +00005034 if num_builders > 10:
5035 confirm_or_exit('There are %d builders with failed builds.'
5036 % num_builders, action='continue')
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00005037 elif options.quick_run:
5038 print('Scheduling CQ quick run on: %s' % cl.GetIssueURL())
5039 return cl.SetCQState(_CQState.QUICK_RUN)
Quinten Yearsley983111f2019-09-26 17:18:48 +00005040 else:
qyearsley1fdfcb62016-10-24 13:22:03 -07005041 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005042 print('git cl try with no bots now defaults to CQ dry run.')
5043 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5044 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005045
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005046 patchset = cl.GetMostRecentPatchset()
Edward Lemur2c210a42019-09-16 23:58:35 +00005047 try:
Quinten Yearsley777660f2020-03-04 23:37:06 +00005048 _trigger_tryjobs(cl, jobs, options, patchset)
Edward Lemur2c210a42019-09-16 23:58:35 +00005049 except BuildbucketResponseException as ex:
5050 print('ERROR: %s' % ex)
5051 return 1
5052 return 0
maruel@chromium.org15192402012-09-06 12:38:29 +00005053
5054
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005055@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005056def CMDtry_results(parser, args):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00005057 """Prints info about results for tryjobs associated with the current CL."""
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005058 group = optparse.OptionGroup(parser, 'Tryjob results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005059 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005060 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005061 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005062 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005063 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005064 '--color', action='store_true', default=setup_color.IS_TTY,
5065 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005066 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005067 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5068 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005069 group.add_option(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005070 '--json', help=('Path of JSON output file to write tryjob results to,'
Stefan Zager1306bd02017-06-22 19:26:46 -07005071 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005072 parser.add_option_group(group)
Edward Lemur52969c92020-02-06 18:15:28 +00005073 parser.add_option(
5074 '-i', '--issue', type=int,
5075 help='Operate on this issue instead of the current branch\'s implicit '
5076 'issue.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005077 options, args = parser.parse_args(args)
5078 if args:
5079 parser.error('Unrecognized args: %s' % ' '.join(args))
5080
Edward Lemur934836a2019-09-09 20:16:54 +00005081 cl = Changelist(issue=options.issue)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005082 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005083 parser.error('Need to upload first.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005084
tandrii221ab252016-10-06 08:12:04 -07005085 patchset = options.patchset
5086 if not patchset:
Gavin Make61ccc52020-11-13 00:12:57 +00005087 patchset = cl.GetMostRecentDryRunPatchset()
tandrii221ab252016-10-06 08:12:04 -07005088 if not patchset:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005089 parser.error('Code review host doesn\'t know about issue %s. '
tandrii221ab252016-10-06 08:12:04 -07005090 'No access to issue or wrong issue number?\n'
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005091 'Either upload first, or pass --patchset explicitly.' %
tandrii221ab252016-10-06 08:12:04 -07005092 cl.GetIssue())
5093
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005094 try:
Quinten Yearsley777660f2020-03-04 23:37:06 +00005095 jobs = _fetch_tryjobs(cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005096 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005097 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005098 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005099 if options.json:
Edward Lemurbaaf6be2019-10-09 18:00:44 +00005100 write_json(options.json, jobs)
qyearsley53f48a12016-09-01 10:45:13 -07005101 else:
Quinten Yearsley777660f2020-03-04 23:37:06 +00005102 _print_tryjobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005103 return 0
5104
5105
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005106@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005107@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005108def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005109 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005110 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005111 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005112 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005113
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005114 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005115 if args:
5116 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005117 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005118 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005119 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005120 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005121
5122 # Clear configured merge-base, if there is one.
5123 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005124 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005125 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005126 return 0
5127
5128
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005129@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00005130def CMDweb(parser, args):
5131 """Opens the current CL in the web browser."""
Orr Bernstein0b960582022-12-22 20:16:18 +00005132 parser.add_option('-p',
5133 '--print-only',
5134 action='store_true',
5135 dest='print_only',
5136 help='Only print the Gerrit URL, don\'t open it in the '
5137 'browser.')
5138 (options, args) = parser.parse_args(args)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005139 if args:
5140 parser.error('Unrecognized args: %s' % ' '.join(args))
5141
5142 issue_url = Changelist().GetIssueURL()
5143 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005144 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005145 return 1
5146
Orr Bernstein0b960582022-12-22 20:16:18 +00005147 if options.print_only:
5148 print(issue_url)
5149 return 0
5150
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005151 # Redirect I/O before invoking browser to hide its output. For example, this
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005152 # allows us to hide the "Created new window in existing browser session."
5153 # message from Chrome. Based on https://stackoverflow.com/a/2323563.
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005154 saved_stdout = os.dup(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005155 saved_stderr = os.dup(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005156 os.close(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005157 os.close(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005158 os.open(os.devnull, os.O_RDWR)
5159 try:
5160 webbrowser.open(issue_url)
5161 finally:
5162 os.dup2(saved_stdout, 1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005163 os.dup2(saved_stderr, 2)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005164 return 0
5165
5166
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005167@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005168def CMDset_commit(parser, args):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00005169 """Sets the commit bit to trigger the CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005170 parser.add_option('-d', '--dry-run', action='store_true',
5171 help='trigger in dry run mode')
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00005172 parser.add_option(
5173 '-q',
5174 '--quick-run',
5175 action='store_true',
5176 help='trigger in quick run mode '
5177 '(https://source.chromium.org/chromium/chromium/src/+/main:docs/cq_qu'
5178 'ick_run.md) (chromium only).')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005179 parser.add_option('-c', '--clear', action='store_true',
5180 help='stop CQ run, if any')
Edward Lemur52969c92020-02-06 18:15:28 +00005181 parser.add_option(
5182 '-i', '--issue', type=int,
5183 help='Operate on this issue instead of the current branch\'s implicit '
5184 'issue.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005185 options, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005186 if args:
5187 parser.error('Unrecognized args: %s' % ' '.join(args))
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00005188 if [options.dry_run, options.quick_run, options.clear].count(True) > 1:
5189 parser.error('Only one of --dry-run, --quick-run, and --clear are allowed.')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005190
Edward Lemur934836a2019-09-09 20:16:54 +00005191 cl = Changelist(issue=options.issue)
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00005192 if not cl.GetIssue():
5193 parser.error('Must upload the issue first.')
5194
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005195 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005196 state = _CQState.NONE
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00005197 elif options.quick_run:
5198 state = _CQState.QUICK_RUN
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005199 elif options.dry_run:
5200 state = _CQState.DRY_RUN
5201 else:
5202 state = _CQState.COMMIT
tandrii9de9ec62016-07-13 03:01:59 -07005203 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005204 return 0
5205
5206
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005207@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005208def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005209 """Closes the issue."""
Edward Lemur52969c92020-02-06 18:15:28 +00005210 parser.add_option(
5211 '-i', '--issue', type=int,
5212 help='Operate on this issue instead of the current branch\'s implicit '
5213 'issue.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005214 options, args = parser.parse_args(args)
groby@chromium.org411034a2013-02-26 15:12:01 +00005215 if args:
5216 parser.error('Unrecognized args: %s' % ' '.join(args))
Edward Lemur934836a2019-09-09 20:16:54 +00005217 cl = Changelist(issue=options.issue)
groby@chromium.org411034a2013-02-26 15:12:01 +00005218 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005219 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005220 DieWithError('ERROR: No issue to close.')
groby@chromium.org411034a2013-02-26 15:12:01 +00005221 cl.CloseIssue()
5222 return 0
5223
5224
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005225@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005226def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005227 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005228 parser.add_option(
5229 '--stat',
5230 action='store_true',
5231 dest='stat',
5232 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005233 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005234 if args:
5235 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005236
Edward Lemur934836a2019-09-09 20:16:54 +00005237 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005238 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005239 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005240 if not issue:
5241 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005242
Gavin Makbe2e9262022-11-08 23:41:55 +00005243 base = cl._GitGetBranchConfigValue(LAST_UPLOAD_HASH_CONFIG_KEY)
Aaron Gablea718c3e2017-08-28 17:47:28 -07005244 if not base:
Gavin Makbe2e9262022-11-08 23:41:55 +00005245 base = cl._GitGetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY)
Aaron Gablea718c3e2017-08-28 17:47:28 -07005246 if not base:
5247 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5248 revision_info = detail['revisions'][detail['current_revision']]
5249 fetch_info = revision_info['fetch']['http']
5250 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5251 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005252
Aaron Gablea718c3e2017-08-28 17:47:28 -07005253 cmd = ['git', 'diff']
5254 if options.stat:
5255 cmd.append('--stat')
5256 cmd.append(base)
5257 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005258
5259 return 0
5260
5261
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005262@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005263def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005264 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005265 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005266 '--ignore-current',
5267 action='store_true',
5268 help='Ignore the CL\'s current reviewers and start from scratch.')
5269 parser.add_option(
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005270 '--ignore-self',
5271 action='store_true',
5272 help='Do not consider CL\'s author as an owners.')
5273 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005274 '--no-color',
5275 action='store_true',
5276 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005277 parser.add_option(
5278 '--batch',
5279 action='store_true',
5280 help='Do not run interactively, just suggest some')
Yang Guo6e269a02019-06-26 11:17:02 +00005281 # TODO: Consider moving this to another command, since other
5282 # git-cl owners commands deal with owners for a given CL.
5283 parser.add_option(
5284 '--show-all',
5285 action='store_true',
5286 help='Show all owners for a particular file')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005287 options, args = parser.parse_args(args)
5288
Edward Lemur934836a2019-09-09 20:16:54 +00005289 cl = Changelist()
Edward Lesmes50da7702020-03-30 19:23:43 +00005290 author = cl.GetAuthor()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005291
Yang Guo6e269a02019-06-26 11:17:02 +00005292 if options.show_all:
Bruce Dawson97ed44a2020-05-06 17:04:03 +00005293 if len(args) == 0:
5294 print('No files specified for --show-all. Nothing to do.')
5295 return 0
Edward Lesmese1576912021-02-16 21:53:34 +00005296 owners_by_path = cl.owners_client.BatchListOwners(args)
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +00005297 for path in args:
5298 print('Owners for %s:' % path)
5299 print('\n'.join(
5300 ' - %s' % owner
5301 for owner in owners_by_path.get(path, ['No owners found'])))
Yang Guo6e269a02019-06-26 11:17:02 +00005302 return 0
5303
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005304 if args:
5305 if len(args) > 1:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005306 parser.error('Unknown args.')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005307 base_branch = args[0]
5308 else:
5309 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005310 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005311
Edward Lemur2c62b332020-03-12 22:12:33 +00005312 affected_files = cl.GetAffectedFiles(base_branch)
Dirk Prankebf980882017-09-02 15:08:00 -07005313
5314 if options.batch:
Edward Lesmese1576912021-02-16 21:53:34 +00005315 owners = cl.owners_client.SuggestOwners(affected_files, exclude=[author])
5316 print('\n'.join(owners))
Dirk Prankebf980882017-09-02 15:08:00 -07005317 return 0
5318
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005319 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005320 affected_files,
Edward Lemur707d70b2018-02-07 00:50:14 +01005321 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005322 [] if options.ignore_current else cl.GetReviewers(),
Edward Lesmes5cd75472021-02-19 00:34:25 +00005323 cl.owners_client,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005324 disable_color=options.no_color,
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005325 ignore_author=options.ignore_self).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005326
5327
Aiden Bennerc08566e2018-10-03 17:52:42 +00005328def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005329 """Generates a diff command."""
5330 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005331 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
5332
Aiden Benner6c18a1a2018-11-23 20:18:23 +00005333 if allow_prefix:
5334 # explicitly setting --src-prefix and --dst-prefix is necessary in the
5335 # case that diff.noprefix is set in the user's git config.
5336 diff_cmd += ['--src-prefix=a/', '--dst-prefix=b/']
5337 else:
Aiden Bennerc08566e2018-10-03 17:52:42 +00005338 diff_cmd += ['--no-prefix']
5339
5340 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005341
5342 if args:
5343 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005344 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005345 diff_cmd.append(arg)
5346 else:
5347 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005348
5349 return diff_cmd
5350
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005351
Jamie Madill5e96ad12020-01-13 16:08:35 +00005352def _RunClangFormatDiff(opts, clang_diff_files, top_dir, upstream_commit):
5353 """Runs clang-format-diff and sets a return value if necessary."""
5354
5355 if not clang_diff_files:
5356 return 0
5357
5358 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5359 # formatted. This is used to block during the presubmit.
5360 return_value = 0
5361
5362 # Locate the clang-format binary in the checkout
5363 try:
5364 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
5365 except clang_format.NotFoundError as e:
5366 DieWithError(e)
5367
5368 if opts.full or settings.GetFormatFullByDefault():
5369 cmd = [clang_format_tool]
5370 if not opts.dry_run and not opts.diff:
5371 cmd.append('-i')
5372 if opts.dry_run:
5373 for diff_file in clang_diff_files:
5374 with open(diff_file, 'r') as myfile:
5375 code = myfile.read().replace('\r\n', '\n')
5376 stdout = RunCommand(cmd + [diff_file], cwd=top_dir)
5377 stdout = stdout.replace('\r\n', '\n')
5378 if opts.diff:
5379 sys.stdout.write(stdout)
5380 if code != stdout:
5381 return_value = 2
5382 else:
5383 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
5384 if opts.diff:
5385 sys.stdout.write(stdout)
5386 else:
Jamie Madill5e96ad12020-01-13 16:08:35 +00005387 try:
5388 script = clang_format.FindClangFormatScriptInChromiumTree(
5389 'clang-format-diff.py')
5390 except clang_format.NotFoundError as e:
5391 DieWithError(e)
5392
Josip Sokcevic2a827fc2022-03-04 17:51:47 +00005393 cmd = ['vpython3', script, '-p0']
Jamie Madill5e96ad12020-01-13 16:08:35 +00005394 if not opts.dry_run and not opts.diff:
5395 cmd.append('-i')
5396
5397 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
Edward Lemur1a83da12020-03-04 21:18:36 +00005398 diff_output = RunGit(diff_cmd).encode('utf-8')
Jamie Madill5e96ad12020-01-13 16:08:35 +00005399
Edward Lesmes89624cd2020-04-06 17:51:56 +00005400 env = os.environ.copy()
5401 env['PATH'] = (
5402 str(os.path.dirname(clang_format_tool)) + os.pathsep + env['PATH'])
5403 stdout = RunCommand(
5404 cmd, stdin=diff_output, cwd=top_dir, env=env,
Ilya Sherman7aed4bb2020-05-20 22:34:14 +00005405 shell=sys.platform.startswith('win32'))
Jamie Madill5e96ad12020-01-13 16:08:35 +00005406 if opts.diff:
5407 sys.stdout.write(stdout)
5408 if opts.dry_run and len(stdout) > 0:
5409 return_value = 2
5410
5411 return return_value
5412
5413
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00005414def _RunRustFmt(opts, rust_diff_files, top_dir, upstream_commit):
5415 """Runs rustfmt. Just like _RunClangFormatDiff returns 2 to indicate that
5416 presubmit checks have failed (and returns 0 otherwise)."""
5417
5418 if not rust_diff_files:
5419 return 0
5420
5421 # Locate the rustfmt binary.
5422 try:
5423 rustfmt_tool = rustfmt.FindRustfmtToolInChromiumTree()
5424 except rustfmt.NotFoundError as e:
5425 DieWithError(e)
5426
5427 # TODO(crbug.com/1231317): Support formatting only the changed lines
5428 # if `opts.full or settings.GetFormatFullByDefault()` is False. See also:
5429 # https://github.com/emilio/rustfmt-format-diff
5430 cmd = [rustfmt_tool]
5431 if opts.dry_run:
5432 cmd.append('--check')
5433 cmd += rust_diff_files
5434 rustfmt_exitcode = subprocess2.call(cmd)
5435
5436 if opts.presubmit and rustfmt_exitcode != 0:
5437 return 2
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00005438
5439 return 0
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00005440
5441
Olivier Robin0a6b5442022-04-07 07:25:04 +00005442def _RunSwiftFormat(opts, swift_diff_files, top_dir, upstream_commit):
5443 """Runs swift-format. Just like _RunClangFormatDiff returns 2 to indicate
5444 that presubmit checks have failed (and returns 0 otherwise)."""
5445
5446 if not swift_diff_files:
5447 return 0
5448
5449 # Locate the swift-format binary.
5450 try:
5451 swift_format_tool = swift_format.FindSwiftFormatToolInChromiumTree()
5452 except swift_format.NotFoundError as e:
5453 DieWithError(e)
5454
5455 cmd = [swift_format_tool]
5456 if opts.dry_run:
Olivier Robin7f39e3d2022-04-28 08:20:49 +00005457 cmd += ['lint', '-s']
Olivier Robin0a6b5442022-04-07 07:25:04 +00005458 else:
5459 cmd += ['format', '-i']
5460 cmd += swift_diff_files
5461 swift_format_exitcode = subprocess2.call(cmd)
5462
5463 if opts.presubmit and swift_format_exitcode != 0:
5464 return 2
5465
5466 return 0
5467
5468
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005469def MatchingFileType(file_name, extensions):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00005470 """Returns True if the file name ends with one of the given extensions."""
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005471 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005472
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005473
enne@chromium.org555cfe42014-01-29 18:21:39 +00005474@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005475@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005476def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005477 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005478 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005479 GN_EXTS = ['.gn', '.gni', '.typemap']
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00005480 RUST_EXTS = ['.rs']
Olivier Robin0a6b5442022-04-07 07:25:04 +00005481 SWIFT_EXTS = ['.swift']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005482 parser.add_option('--full', action='store_true',
5483 help='Reformat the full content of all touched files')
Tomasz Åšniatowski58194462021-08-27 17:36:16 +00005484 parser.add_option('--upstream', help='Branch to check against')
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005485 parser.add_option('--dry-run', action='store_true',
5486 help='Don\'t modify any file on disk.')
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005487 parser.add_option(
Garrett Beatyed0cc5f2020-01-06 17:26:13 +00005488 '--no-clang-format',
5489 dest='clang_format',
5490 action='store_false',
5491 default=True,
5492 help='Disables formatting of various file types using clang-format.')
5493 parser.add_option(
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005494 '--python',
5495 action='store_true',
5496 default=None,
5497 help='Enables python formatting on all python files.')
5498 parser.add_option(
5499 '--no-python',
5500 action='store_true',
Garrett Beaty91a6f332020-01-06 16:57:24 +00005501 default=False,
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005502 help='Disables python formatting on all python files. '
Garrett Beaty91a6f332020-01-06 16:57:24 +00005503 'If neither --python or --no-python are set, python files that have a '
5504 '.style.yapf file in an ancestor directory will be formatted. '
5505 'It is an error to set both.')
Garrett Beatyed0cc5f2020-01-06 17:26:13 +00005506 parser.add_option(
5507 '--js',
5508 action='store_true',
5509 help='Format javascript code with clang-format. '
5510 'Has no effect if --no-clang-format is set.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005511 parser.add_option('--diff', action='store_true',
5512 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005513 parser.add_option('--presubmit', action='store_true',
5514 help='Used when running the script from a presubmit.')
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00005515
5516 parser.add_option('--rust-fmt',
5517 dest='use_rust_fmt',
5518 action='store_true',
5519 default=rustfmt.IsRustfmtSupported(),
5520 help='Enables formatting of Rust file types using rustfmt.')
5521 parser.add_option(
5522 '--no-rust-fmt',
5523 dest='use_rust_fmt',
5524 action='store_false',
5525 help='Disables formatting of Rust file types using rustfmt.')
5526
Olivier Robin0a6b5442022-04-07 07:25:04 +00005527 parser.add_option(
5528 '--swift-format',
5529 dest='use_swift_format',
5530 action='store_true',
Olivier Robin7f39e3d2022-04-28 08:20:49 +00005531 default=swift_format.IsSwiftFormatSupported(),
Olivier Robin0a6b5442022-04-07 07:25:04 +00005532 help='Enables formatting of Swift file types using swift-format '
5533 '(macOS host only).')
5534 parser.add_option(
5535 '--no-swift-format',
5536 dest='use_swift_format',
5537 action='store_false',
5538 help='Disables formatting of Swift file types using swift-format.')
5539
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005540 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005541
Garrett Beaty91a6f332020-01-06 16:57:24 +00005542 if opts.python is not None and opts.no_python:
5543 raise parser.error('Cannot set both --python and --no-python')
5544 if opts.no_python:
5545 opts.python = False
5546
Daniel Chengc55eecf2016-12-30 03:11:02 -08005547 # Normalize any remaining args against the current path, so paths relative to
5548 # the current directory are still resolved as expected.
5549 args = [os.path.join(os.getcwd(), arg) for arg in args]
5550
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005551 # git diff generates paths against the root of the repository. Change
5552 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005553 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005554 if rel_base_path:
5555 os.chdir(rel_base_path)
5556
digit@chromium.org29e47272013-05-17 17:01:46 +00005557 # Grab the merge-base commit, i.e. the upstream commit of the current
5558 # branch when it was created or the last time it was rebased. This is
5559 # to cover the case where the user may have called "git fetch origin",
5560 # moving the origin branch to a newer commit, but hasn't rebased yet.
5561 upstream_commit = None
Tomasz Åšniatowski58194462021-08-27 17:36:16 +00005562 upstream_branch = opts.upstream
5563 if not upstream_branch:
5564 cl = Changelist()
5565 upstream_branch = cl.GetUpstreamBranch()
digit@chromium.org29e47272013-05-17 17:01:46 +00005566 if upstream_branch:
5567 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5568 upstream_commit = upstream_commit.strip()
5569
5570 if not upstream_commit:
5571 DieWithError('Could not find base commit for this branch. '
5572 'Are you in detached state?')
5573
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005574 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5575 diff_output = RunGit(changed_files_cmd)
5576 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005577 # Filter out files deleted by this CL
5578 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005579
Andreas Haas417d89c2020-02-06 10:24:27 +00005580 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00005581 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11005582
Garrett Beatyed0cc5f2020-01-06 17:26:13 +00005583 clang_diff_files = []
5584 if opts.clang_format:
5585 clang_diff_files = [
5586 x for x in diff_files if MatchingFileType(x, CLANG_EXTS)
5587 ]
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005588 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00005589 rust_diff_files = [x for x in diff_files if MatchingFileType(x, RUST_EXTS)]
Olivier Robin0a6b5442022-04-07 07:25:04 +00005590 swift_diff_files = [x for x in diff_files if MatchingFileType(x, SWIFT_EXTS)]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005591 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005592
Edward Lesmes50da7702020-03-30 19:23:43 +00005593 top_dir = settings.GetRoot()
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005594
Jamie Madill5e96ad12020-01-13 16:08:35 +00005595 return_value = _RunClangFormatDiff(opts, clang_diff_files, top_dir,
5596 upstream_commit)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005597
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00005598 if opts.use_rust_fmt:
5599 rust_fmt_return_value = _RunRustFmt(opts, rust_diff_files, top_dir,
5600 upstream_commit)
5601 if rust_fmt_return_value == 2:
5602 return_value = 2
5603
Olivier Robin0a6b5442022-04-07 07:25:04 +00005604 if opts.use_swift_format:
5605 if sys.platform != 'darwin':
5606 DieWithError('swift-format is only supported on macOS.')
5607 swift_format_return_value = _RunSwiftFormat(opts, swift_diff_files, top_dir,
5608 upstream_commit)
5609 if swift_format_return_value == 2:
5610 return_value = 2
5611
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005612 # Similar code to above, but using yapf on .py files rather than clang-format
5613 # on C/C++ files
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005614 py_explicitly_disabled = opts.python is not None and not opts.python
5615 if python_diff_files and not py_explicitly_disabled:
Aiden Bennere47ac152018-11-20 16:44:03 +00005616 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
5617 yapf_tool = os.path.join(depot_tools_path, 'yapf')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005618
Aiden Bennerc08566e2018-10-03 17:52:42 +00005619 # Used for caching.
5620 yapf_configs = {}
5621 for f in python_diff_files:
5622 # Find the yapf style config for the current file, defaults to depot
5623 # tools default.
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005624 _FindYapfConfigFile(f, yapf_configs, top_dir)
5625
5626 # Turn on python formatting by default if a yapf config is specified.
5627 # This breaks in the case of this repo though since the specified
5628 # style file is also the global default.
5629 if opts.python is None:
5630 filtered_py_files = []
5631 for f in python_diff_files:
5632 if _FindYapfConfigFile(f, yapf_configs, top_dir) is not None:
5633 filtered_py_files.append(f)
5634 else:
5635 filtered_py_files = python_diff_files
5636
5637 # Note: yapf still seems to fix indentation of the entire file
5638 # even if line ranges are specified.
5639 # See https://github.com/google/yapf/issues/499
5640 if not opts.full and filtered_py_files:
5641 py_line_diffs = _ComputeDiffLineRanges(filtered_py_files, upstream_commit)
5642
Brian Sheedyb4307d52019-12-02 19:18:17 +00005643 yapfignore_patterns = _GetYapfIgnorePatterns(top_dir)
5644 filtered_py_files = _FilterYapfIgnoredFiles(filtered_py_files,
5645 yapfignore_patterns)
Brian Sheedy59b06a82019-10-14 17:03:29 +00005646
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005647 for f in filtered_py_files:
Andrew Grievefa40bfa2020-01-07 02:32:57 +00005648 yapf_style = _FindYapfConfigFile(f, yapf_configs, top_dir)
5649 # Default to pep8 if not .style.yapf is found.
5650 if not yapf_style:
5651 yapf_style = 'pep8'
Aiden Bennerc08566e2018-10-03 17:52:42 +00005652
Peter Wend9399922020-06-17 17:33:49 +00005653 with open(f, 'r') as py_f:
Andrew Grieveb9e694c2021-11-15 19:04:46 +00005654 if 'python2' in py_f.readline():
Peter Wend9399922020-06-17 17:33:49 +00005655 vpython_script = 'vpython'
Andrew Grieveb9e694c2021-11-15 19:04:46 +00005656 else:
5657 vpython_script = 'vpython3'
Peter Wend9399922020-06-17 17:33:49 +00005658
5659 cmd = [vpython_script, yapf_tool, '--style', yapf_style, f]
Aiden Bennerc08566e2018-10-03 17:52:42 +00005660
5661 has_formattable_lines = False
5662 if not opts.full:
5663 # Only run yapf over changed line ranges.
5664 for diff_start, diff_len in py_line_diffs[f]:
5665 diff_end = diff_start + diff_len - 1
5666 # Yapf errors out if diff_end < diff_start but this
5667 # is a valid line range diff for a removal.
5668 if diff_end >= diff_start:
5669 has_formattable_lines = True
5670 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
5671 # If all line diffs were removals we have nothing to format.
5672 if not has_formattable_lines:
5673 continue
5674
5675 if opts.diff or opts.dry_run:
5676 cmd += ['--diff']
5677 # Will return non-zero exit code if non-empty diff.
Edward Lesmesb7db1832020-06-22 20:22:27 +00005678 stdout = RunCommand(cmd,
5679 error_ok=True,
Josip Sokcevic673e8ed2021-10-27 23:46:18 +00005680 stderr=subprocess2.PIPE,
Edward Lesmesb7db1832020-06-22 20:22:27 +00005681 cwd=top_dir,
5682 shell=sys.platform.startswith('win32'))
Aiden Bennerc08566e2018-10-03 17:52:42 +00005683 if opts.diff:
5684 sys.stdout.write(stdout)
5685 elif len(stdout) > 0:
5686 return_value = 2
5687 else:
5688 cmd += ['-i']
Edward Lesmesb7db1832020-06-22 20:22:27 +00005689 RunCommand(cmd, cwd=top_dir, shell=sys.platform.startswith('win32'))
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005690
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005691 # Format GN build files. Always run on full build files for canonical form.
5692 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005693 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005694 if opts.dry_run or opts.diff:
5695 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005696 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005697 gn_ret = subprocess2.call(cmd + [gn_diff_file],
Ilya Sherman7aed4bb2020-05-20 22:34:14 +00005698 shell=sys.platform.startswith('win'),
brettw4b8ed592016-08-05 16:19:12 -07005699 cwd=top_dir)
5700 if opts.dry_run and gn_ret == 2:
5701 return_value = 2 # Not formatted.
5702 elif opts.diff and gn_ret == 2:
5703 # TODO this should compute and print the actual diff.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005704 print('This change has GN build file diff for ' + gn_diff_file)
brettw4b8ed592016-08-05 16:19:12 -07005705 elif gn_ret != 0:
5706 # For non-dry run cases (and non-2 return values for dry-run), a
5707 # nonzero error code indicates a failure, probably because the file
5708 # doesn't parse.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005709 DieWithError('gn format failed on ' + gn_diff_file +
5710 '\nTry running `gn format` on this file manually.')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005711
Ilya Shermane081cbe2017-08-15 17:51:04 -07005712 # Skip the metrics formatting from the global presubmit hook. These files have
5713 # a separate presubmit hook that issues an error if the files need formatting,
5714 # whereas the top-level presubmit script merely issues a warning. Formatting
5715 # these files is somewhat slow, so it's important not to duplicate the work.
5716 if not opts.presubmit:
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00005717 for diff_xml in GetDiffXMLs(diff_files):
5718 xml_dir = GetMetricsDir(diff_xml)
5719 if not xml_dir:
5720 continue
5721
Ilya Shermane081cbe2017-08-15 17:51:04 -07005722 tool_dir = os.path.join(top_dir, xml_dir)
Ilya Sherman7aed4bb2020-05-20 22:34:14 +00005723 pretty_print_tool = os.path.join(tool_dir, 'pretty_print.py')
Fabrice de Gansecfab092022-09-15 20:59:01 +00005724 cmd = ['vpython3', pretty_print_tool, '--non-interactive']
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00005725
5726 # If the XML file is histograms.xml or enums.xml, add the xml path to the
5727 # command as histograms/pretty_print.py now needs a relative path argument
5728 # after splitting the histograms into multiple directories.
5729 # For example, in tools/metrics/ukm, pretty-print could be run using:
5730 # $ python pretty_print.py
5731 # But in tools/metrics/histogrmas, pretty-print should be run with an
5732 # additional relative path argument, like:
Peter Kastingee088882021-08-03 17:57:00 +00005733 # $ python pretty_print.py metadata/UMA/histograms.xml
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00005734 # $ python pretty_print.py enums.xml
5735
Weilun Shib92c4b72020-08-27 17:45:11 +00005736 if (diff_xml.endswith('histograms.xml') or diff_xml.endswith('enums.xml')
Weilun Shi4f50adb2023-01-17 20:43:17 +00005737 or diff_xml.endswith('histogram_suffixes_list.xml')):
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00005738 cmd.append(diff_xml)
5739
Ilya Shermane081cbe2017-08-15 17:51:04 -07005740 if opts.dry_run or opts.diff:
5741 cmd.append('--diff')
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00005742
Ilya Sherman7aed4bb2020-05-20 22:34:14 +00005743 # TODO(isherman): Once this file runs only on Python 3.3+, drop the
5744 # `shell` param and instead replace `'vpython'` with
5745 # `shutil.which('frob')` above: https://stackoverflow.com/a/32799942
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00005746 stdout = RunCommand(cmd,
5747 cwd=top_dir,
Ilya Sherman7aed4bb2020-05-20 22:34:14 +00005748 shell=sys.platform.startswith('win32'))
Ilya Shermane081cbe2017-08-15 17:51:04 -07005749 if opts.diff:
5750 sys.stdout.write(stdout)
5751 if opts.dry_run and stdout:
5752 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005753
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005754 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005755
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005756
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00005757def GetDiffXMLs(diff_files):
5758 return [
5759 os.path.normpath(x) for x in diff_files if MatchingFileType(x, ['.xml'])
5760 ]
5761
5762
5763def GetMetricsDir(diff_xml):
Steven Holte2e664bf2017-04-21 13:10:47 -07005764 metrics_xml_dirs = [
5765 os.path.join('tools', 'metrics', 'actions'),
5766 os.path.join('tools', 'metrics', 'histograms'),
5767 os.path.join('tools', 'metrics', 'rappor'),
Ilya Shermanb67e60c2020-05-20 22:27:03 +00005768 os.path.join('tools', 'metrics', 'structured'),
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005769 os.path.join('tools', 'metrics', 'ukm'),
5770 ]
Steven Holte2e664bf2017-04-21 13:10:47 -07005771 for xml_dir in metrics_xml_dirs:
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00005772 if diff_xml.startswith(xml_dir):
5773 return xml_dir
5774 return None
Steven Holte2e664bf2017-04-21 13:10:47 -07005775
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005776
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005777@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005778@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005779def CMDcheckout(parser, args):
Edward Lemurf38bc172019-09-03 21:02:13 +00005780 """Checks out a branch associated with a given Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005781 _, args = parser.parse_args(args)
5782
5783 if len(args) != 1:
5784 parser.print_help()
5785 return 1
5786
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005787 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005788 if not issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005789 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005790
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005791 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005792
Edward Lemur52969c92020-02-06 18:15:28 +00005793 output = RunGit(['config', '--local', '--get-regexp',
Edward Lesmes50da7702020-03-30 19:23:43 +00005794 r'branch\..*\.' + ISSUE_CONFIG_KEY],
Edward Lemur52969c92020-02-06 18:15:28 +00005795 error_ok=True)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005796
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005797 branches = []
Edward Lemur52969c92020-02-06 18:15:28 +00005798 for key, issue in [x.split() for x in output.splitlines()]:
5799 if issue == target_issue:
Edward Lesmes50da7702020-03-30 19:23:43 +00005800 branches.append(re.sub(r'branch\.(.*)\.' + ISSUE_CONFIG_KEY, r'\1', key))
Edward Lemur52969c92020-02-06 18:15:28 +00005801
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005802 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005803 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005804 return 1
5805 if len(branches) == 1:
5806 RunGit(['checkout', branches[0]])
5807 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005808 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005809 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005810 print('%d: %s' % (i, branches[i]))
Edward Lesmesae3586b2020-03-23 21:21:14 +00005811 which = gclient_utils.AskForData('Choose by index: ')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005812 try:
5813 RunGit(['checkout', branches[int(which)]])
5814 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005815 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005816 return 1
5817
5818 return 0
5819
5820
maruel@chromium.org29404b52014-09-08 22:58:00 +00005821def CMDlol(parser, args):
5822 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005823 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005824 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5825 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5826 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
Gavin Mak18f45d22020-12-04 21:45:10 +00005827 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')).decode('utf-8'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005828 return 0
5829
5830
Josip Sokcevic0399e172022-03-21 23:11:51 +00005831def CMDversion(parser, args):
5832 import utils
5833 print(utils.depot_tools_version())
5834
5835
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005836class OptionParser(optparse.OptionParser):
5837 """Creates the option parse and add --verbose support."""
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00005838
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005839 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005840 optparse.OptionParser.__init__(
5841 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005842 self.add_option(
5843 '-v', '--verbose', action='count', default=0,
5844 help='Use 2 times for more debugging info')
5845
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005846 def parse_args(self, args=None, _values=None):
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00005847 try:
5848 return self._parse_args(args)
5849 finally:
5850 # Regardless of success or failure of args parsing, we want to report
5851 # metrics, but only after logging has been initialized (if parsing
5852 # succeeded).
5853 global settings
5854 settings = Settings()
5855
Edward Lesmes9c349062021-05-06 20:02:39 +00005856 if metrics.collector.config.should_collect_metrics:
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00005857 # GetViewVCUrl ultimately calls logging method.
5858 project_url = settings.GetViewVCUrl().strip('/+')
5859 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
5860 metrics.collector.add('project_urls', [project_url])
5861
5862 def _parse_args(self, args=None):
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005863 # Create an optparse.Values object that will store only the actual passed
5864 # options, without the defaults.
5865 actual_options = optparse.Values()
5866 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
5867 # Create an optparse.Values object with the default options.
5868 options = optparse.Values(self.get_default_values().__dict__)
5869 # Update it with the options passed by the user.
5870 options._update_careful(actual_options.__dict__)
5871 # Store the options passed by the user in an _actual_options attribute.
5872 # We store only the keys, and not the values, since the values can contain
5873 # arbitrary information, which might be PII.
Edward Lemur79d4f992019-11-11 23:49:02 +00005874 metrics.collector.add('arguments', list(actual_options.__dict__.keys()))
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005875
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005876 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005877 logging.basicConfig(
5878 level=levels[min(options.verbose, len(levels) - 1)],
5879 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5880 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00005881
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005882 return options, args
5883
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005884
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005885def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005886 if sys.hexversion < 0x02060000:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005887 print('\nYour Python version %s is unsupported, please upgrade.\n' %
vapiera7fbd5a2016-06-16 09:17:49 -07005888 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005889 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005890
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005891 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005892 dispatcher = subcommand.CommandDispatcher(__name__)
5893 try:
5894 return dispatcher.execute(OptionParser(), argv)
Edward Lemur5b929a42019-10-21 17:57:39 +00005895 except auth.LoginRequiredError as e:
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005896 DieWithError(str(e))
Edward Lemur79d4f992019-11-11 23:49:02 +00005897 except urllib.error.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005898 if e.code != 500:
5899 raise
5900 DieWithError(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005901 ('App Engine is misbehaving and returned HTTP %d, again. Keep faith '
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005902 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005903 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005904
5905
5906if __name__ == '__main__':
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005907 # These affect sys.stdout, so do it outside of main() to simplify mocks in
5908 # the unit tests.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005909 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005910 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00005911 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00005912 sys.exit(main(sys.argv[1:]))