blob: 5ba2f4cb579bd7d5bacd0b1e8b884b7c136f8384 [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>
Andrii Shyshkalov03da1502018-10-15 03:42:34 +00007"""A git-command for integrating reviews on Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008
thakis@chromium.org3421c992014-11-02 02:20:32 +00009import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000010import collections
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +010011import datetime
Brian Sheedyb4307d52019-12-02 19:18:17 +000012import fnmatch
Edward Lemur202c5592019-10-21 22:44:52 +000013import httplib2
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +010014import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000015import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000016import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000017import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000018import optparse
19import os
20import re
Andrii Shyshkalov353637c2017-03-14 16:52:18 +010021import shutil
ukai@chromium.org78c4b982012-02-14 02:20:26 +000022import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023import sys
Aaron Gable9a03ae02017-11-03 11:31:07 -070024import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import textwrap
Edward Lemurfec80c42018-11-01 23:14:14 +000026import time
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000027import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000028import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000029import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000030
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000031from third_party import colorama
Daniel Chengabf48472023-08-30 15:45:13 +000032from typing import Any
33from typing import List
34from typing import Mapping
35from typing import NoReturn
Daniel Cheng66d0f152023-08-29 23:21:58 +000036from typing import Optional
37from typing import Sequence
38from typing import Tuple
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000039import auth
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000040import clang_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000041import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000042import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000043import gerrit_util
iannucci@chromium.org9e849272014-04-04 00:31:55 +000044import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000045import git_footers
Edward Lemur85153282020-02-14 22:06:29 +000046import git_new_branch
Edward Lemur5ba1e9c2018-07-23 18:19:02 +000047import metrics
Edward Lesmes93277a72018-10-18 22:04:26 +000048import metrics_utils
Edward Lesmeseeca9c62020-11-20 00:00:17 +000049import owners_client
iannucci@chromium.org9e849272014-04-04 00:31:55 +000050import owners_finder
Lei Zhangb8c62cf2020-07-15 20:09:37 +000051import presubmit_canned_checks
Josip Sokcevic7958e302023-03-01 23:02:21 +000052import presubmit_support
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +000053import rustfmt
Josip Sokcevic7958e302023-03-01 23:02:21 +000054import scm
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +000055import setup_color
Francois Dorayd42c6812017-05-30 15:10:20 -040056import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000057import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000058import subprocess2
Olivier Robin0a6b5442022-04-07 07:25:04 +000059import swift_format
maruel@chromium.org2a74d372011-03-29 19:05:50 +000060import watchlists
61
Edward Lemur79d4f992019-11-11 23:49:02 +000062from six.moves import urllib
63
tandrii7400cf02016-06-21 08:48:07 -070064__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000065
Mike Frysinger124bb8e2023-09-06 05:48:55 +000066# TODO: Should fix these warnings.
67# pylint: disable=line-too-long
68
Edward Lemur0f58ae42019-04-30 17:24:12 +000069# Traces for git push will be stored in a traces directory inside the
70# depot_tools checkout.
71DEPOT_TOOLS = os.path.dirname(os.path.abspath(__file__))
72TRACES_DIR = os.path.join(DEPOT_TOOLS, 'traces')
Edward Lemur227d5102020-02-25 23:45:35 +000073PRESUBMIT_SUPPORT = os.path.join(DEPOT_TOOLS, 'presubmit_support.py')
Edward Lemur0f58ae42019-04-30 17:24:12 +000074
75# When collecting traces, Git hashes will be reduced to 6 characters to reduce
76# the size after compression.
77GIT_HASH_RE = re.compile(r'\b([a-f0-9]{6})[a-f0-9]{34}\b', flags=re.I)
78# Used to redact the cookies from the gitcookies file.
79GITCOOKIES_REDACT_RE = re.compile(r'1/.*')
80
Edward Lemurd4d1ba42019-09-20 21:46:37 +000081MAX_ATTEMPTS = 3
82
Edward Lemur1b52d872019-05-09 21:12:12 +000083# The maximum number of traces we will keep. Multiplied by 3 since we store
84# 3 files per trace.
85MAX_TRACES = 3 * 10
Edward Lemur5737f022019-05-17 01:24:00 +000086# Message to be displayed to the user to inform where to find the traces for a
87# git-cl upload execution.
Edward Lemur0f58ae42019-04-30 17:24:12 +000088TRACES_MESSAGE = (
Mike Frysinger124bb8e2023-09-06 05:48:55 +000089 '\n'
90 'The traces of this git-cl execution have been recorded at:\n'
91 ' %(trace_name)s-traces.zip\n'
92 'Copies of your gitcookies file and git config have been recorded at:\n'
93 ' %(trace_name)s-git-info.zip\n')
Edward Lemur1b52d872019-05-09 21:12:12 +000094# Format of the message to be stored as part of the traces to give developers a
95# better context when they go through traces.
Mike Frysinger124bb8e2023-09-06 05:48:55 +000096TRACES_README_FORMAT = ('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
Joanna Wanga8db0cb2023-01-24 15:43:17 +0000116DEFAULT_BUILDBUCKET_HOST = 'cr-buildbucket.appspot.com'
117
thestig@chromium.org44202a22014-03-11 19:22:18 +0000118# Valid extensions for files we want to lint.
119DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
120DEFAULT_LINT_IGNORE_REGEX = r"$^"
121
Aiden Bennerc08566e2018-10-03 17:52:42 +0000122# File name for yapf style config files.
123YAPF_CONFIG_FILENAME = '.style.yapf'
124
Edward Lesmes50da7702020-03-30 19:23:43 +0000125# The issue, patchset and codereview server are stored on git config for each
126# branch under branch.<branch-name>.<config-key>.
127ISSUE_CONFIG_KEY = 'gerritissue'
128PATCHSET_CONFIG_KEY = 'gerritpatchset'
129CODEREVIEW_SERVER_CONFIG_KEY = 'gerritserver'
Gavin Makbe2e9262022-11-08 23:41:55 +0000130# When using squash workflow, _CMDUploadChange doesn't simply push the commit(s)
131# you make to Gerrit. Instead, it creates a new commit object that contains all
132# changes you've made, diffed against a parent/merge base.
133# This is the hash of the new squashed commit and you can find this on Gerrit.
134GERRIT_SQUASH_HASH_CONFIG_KEY = 'gerritsquashhash'
135# This is the latest uploaded local commit hash.
136LAST_UPLOAD_HASH_CONFIG_KEY = 'last-upload-hash'
Edward Lesmes50da7702020-03-30 19:23:43 +0000137
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000138# Shortcut since it quickly becomes repetitive.
maruel@chromium.org2e23ce32013-05-07 12:42:28 +0000139Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +0000140
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000141# Initialized in main()
142settings = None
143
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100144# Used by tests/git_cl_test.py to add extra logging.
145# Inside the weirdly failing test, add this:
146# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700147# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100148_IS_BEING_TESTED = False
149
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000150_GOOGLESOURCE = 'googlesource.com'
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000151
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +0000152_KNOWN_GERRIT_TO_SHORT_URLS = {
153 'https://chrome-internal-review.googlesource.com': 'https://crrev.com/i',
154 'https://chromium-review.googlesource.com': 'https://crrev.com/c',
155}
Andrii Shyshkalov8aebb602020-04-16 22:10:27 +0000156assert len(_KNOWN_GERRIT_TO_SHORT_URLS) == len(
157 set(_KNOWN_GERRIT_TO_SHORT_URLS.values())), 'must have unique values'
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +0000158
Joanna Wang18de1f62023-01-21 01:24:24 +0000159# Maximum number of branches in a stack that can be traversed and uploaded
160# at once. Picked arbitrarily.
161_MAX_STACKED_BRANCHES_UPLOAD = 20
162
Joanna Wang892f2ce2023-03-14 21:39:47 +0000163# Environment variable to indicate if user is participating in the stcked
164# changes dogfood.
165DOGFOOD_STACKED_CHANGES_VAR = 'DOGFOOD_STACKED_CHANGES'
166
167
Josip Sokcevicf736cab2020-10-20 23:41:38 +0000168class GitPushError(Exception):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000169 pass
Josip Sokcevicf736cab2020-10-20 23:41:38 +0000170
171
Daniel Chengabf48472023-08-30 15:45:13 +0000172def DieWithError(message, change_desc=None) -> NoReturn:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000173 if change_desc:
174 SaveDescriptionBackup(change_desc)
175 print('\n ** Content of CL description **\n' + '=' * 72 + '\n' +
176 change_desc.description + '\n' + '=' * 72 + '\n')
Christopher Lamf732cd52017-01-24 12:40:11 +1100177
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000178 print(message, file=sys.stderr)
179 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000180
181
Christopher Lamf732cd52017-01-24 12:40:11 +1100182def SaveDescriptionBackup(change_desc):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000183 backup_path = os.path.join(DEPOT_TOOLS, DESCRIPTION_BACKUP_FILE)
184 print('\nsaving CL description to %s\n' % backup_path)
185 with open(backup_path, 'wb') as backup_file:
186 backup_file.write(change_desc.description.encode('utf-8'))
Christopher Lamf732cd52017-01-24 12:40:11 +1100187
188
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000189def GetNoGitPagerEnv():
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000190 env = os.environ.copy()
191 # 'cat' is a magical git string that disables pagers on all platforms.
192 env['GIT_PAGER'] = 'cat'
193 return env
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000194
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000195
bsep@chromium.org627d9002016-04-29 00:00:52 +0000196def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000197 try:
198 stdout = subprocess2.check_output(args, shell=shell, **kwargs)
199 return stdout.decode('utf-8', 'replace')
200 except subprocess2.CalledProcessError as e:
201 logging.debug('Failed running %s', args)
202 if not error_ok:
203 message = error_message or e.stdout.decode('utf-8', 'replace') or ''
204 DieWithError('Command "%s" failed.\n%s' % (' '.join(args), message))
205 out = e.stdout.decode('utf-8', 'replace')
206 if e.stderr:
207 out += e.stderr.decode('utf-8', 'replace')
208 return out
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000209
210
211def RunGit(args, **kwargs):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000212 """Returns stdout."""
213 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000214
215
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000216def RunGitWithCode(args, suppress_stderr=False):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000217 """Returns return code and stdout."""
218 if suppress_stderr:
219 stderr = subprocess2.DEVNULL
220 else:
221 stderr = sys.stderr
222 try:
223 (out, _), code = subprocess2.communicate(['git'] + args,
224 env=GetNoGitPagerEnv(),
225 stdout=subprocess2.PIPE,
226 stderr=stderr)
227 return code, out.decode('utf-8', 'replace')
228 except subprocess2.CalledProcessError as e:
229 logging.debug('Failed running %s', ['git'] + args)
230 return e.returncode, e.stdout.decode('utf-8', 'replace')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000231
232
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000233def RunGitSilent(args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000234 """Returns stdout, suppresses stderr and ignores the return code."""
235 return RunGitWithCode(args, suppress_stderr=True)[1]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000236
237
tandrii2a16b952016-10-19 07:09:44 -0700238def time_sleep(seconds):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000239 # Use this so that it can be mocked in tests without interfering with python
240 # system machinery.
241 return time.sleep(seconds)
tandrii2a16b952016-10-19 07:09:44 -0700242
243
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000244def time_time():
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000245 # Use this so that it can be mocked in tests without interfering with python
246 # system machinery.
247 return time.time()
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000248
249
Edward Lemur1b52d872019-05-09 21:12:12 +0000250def datetime_now():
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000251 # Use this so that it can be mocked in tests without interfering with python
252 # system machinery.
253 return datetime.datetime.now()
Edward Lemur1b52d872019-05-09 21:12:12 +0000254
255
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100256def confirm_or_exit(prefix='', action='confirm'):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000257 """Asks user to press enter to continue or press Ctrl+C to abort."""
258 if not prefix or prefix.endswith('\n'):
259 mid = 'Press'
260 elif prefix.endswith('.') or prefix.endswith('?'):
261 mid = ' Press'
262 elif prefix.endswith(' '):
263 mid = 'press'
264 else:
265 mid = ' press'
266 gclient_utils.AskForData('%s%s Enter to %s, or Ctrl+C to abort' %
267 (prefix, mid, action))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100268
269
270def ask_for_explicit_yes(prompt):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000271 """Returns whether user typed 'y' or 'yes' to confirm the given prompt."""
272 result = gclient_utils.AskForData(prompt + ' [Yes/No]: ').lower()
273 while True:
274 if 'yes'.startswith(result):
275 return True
276 if 'no'.startswith(result):
277 return False
278 result = gclient_utils.AskForData('Please, type yes or no: ').lower()
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100279
280
machenbach@chromium.org45453142015-09-15 08:45:22 +0000281def _get_properties_from_options(options):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000282 prop_list = getattr(options, 'properties', [])
283 properties = dict(x.split('=', 1) for x in prop_list)
284 for key, val in properties.items():
285 try:
286 properties[key] = json.loads(val)
287 except ValueError:
288 pass # If a value couldn't be evaluated, treat it as a string.
289 return properties
machenbach@chromium.org45453142015-09-15 08:45:22 +0000290
291
Edward Lemur4c707a22019-09-24 21:13:43 +0000292def _call_buildbucket(http, buildbucket_host, method, request):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000293 """Calls a buildbucket v2 method and returns the parsed json response."""
294 headers = {
295 'Accept': 'application/json',
296 'Content-Type': 'application/json',
297 }
298 request = json.dumps(request)
299 url = 'https://%s/prpc/buildbucket.v2.Builds/%s' % (buildbucket_host,
300 method)
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000301
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000302 logging.info('POST %s with %s' % (url, request))
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000303
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000304 attempts = 1
305 time_to_sleep = 1
306 while True:
307 response, content = http.request(url,
308 'POST',
309 body=request,
310 headers=headers)
311 if response.status == 200:
312 return json.loads(content[4:])
313 if attempts >= MAX_ATTEMPTS or 400 <= response.status < 500:
314 msg = '%s error when calling POST %s with %s: %s' % (
315 response.status, url, request, content)
316 raise BuildbucketResponseException(msg)
317 logging.debug('%s error when calling POST %s with %s. '
318 'Sleeping for %d seconds and retrying...' %
319 (response.status, url, request, time_to_sleep))
320 time.sleep(time_to_sleep)
321 time_to_sleep *= 2
322 attempts += 1
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000323
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000324 assert False, 'unreachable'
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000325
326
Edward Lemur6215c792019-10-03 21:59:05 +0000327def _parse_bucket(raw_bucket):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000328 legacy = True
329 project = bucket = None
330 if '/' in raw_bucket:
331 legacy = False
332 project, bucket = raw_bucket.split('/', 1)
333 # Assume luci.<project>.<bucket>.
334 elif raw_bucket.startswith('luci.'):
335 project, bucket = raw_bucket[len('luci.'):].split('.', 1)
336 # Otherwise, assume prefix is also the project name.
337 elif '.' in raw_bucket:
338 project = raw_bucket.split('.')[0]
339 bucket = raw_bucket
340 # Legacy buckets.
341 if legacy and project and bucket:
342 print('WARNING Please use %s/%s to specify the bucket.' %
343 (project, bucket))
344 return project, bucket
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000345
346
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000347def _canonical_git_googlesource_host(host):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000348 """Normalizes Gerrit hosts (with '-review') to Git host."""
349 assert host.endswith(_GOOGLESOURCE)
350 # Prefix doesn't include '.' at the end.
351 prefix = host[:-(1 + len(_GOOGLESOURCE))]
352 if prefix.endswith('-review'):
353 prefix = prefix[:-len('-review')]
354 return prefix + '.' + _GOOGLESOURCE
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000355
356
357def _canonical_gerrit_googlesource_host(host):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000358 git_host = _canonical_git_googlesource_host(host)
359 prefix = git_host.split('.', 1)[0]
360 return prefix + '-review.' + _GOOGLESOURCE
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000361
362
363def _get_counterpart_host(host):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000364 assert host.endswith(_GOOGLESOURCE)
365 git = _canonical_git_googlesource_host(host)
366 gerrit = _canonical_gerrit_googlesource_host(git)
367 return git if gerrit == host else gerrit
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000368
369
Quinten Yearsley777660f2020-03-04 23:37:06 +0000370def _trigger_tryjobs(changelist, jobs, options, patchset):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000371 """Sends a request to Buildbucket to trigger tryjobs for a changelist.
qyearsley1fdfcb62016-10-24 13:22:03 -0700372
373 Args:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000374 changelist: Changelist that the tryjobs are associated with.
Edward Lemur45768512020-03-02 19:03:14 +0000375 jobs: A list of (project, bucket, builder).
qyearsley1fdfcb62016-10-24 13:22:03 -0700376 options: Command-line options.
377 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000378 print('Scheduling jobs on:')
379 for project, bucket, builder in jobs:
380 print(' %s/%s: %s' % (project, bucket, builder))
381 print('To see results here, run: git cl try-results')
382 print('To see results in browser, run: git cl web')
tandriide281ae2016-10-12 06:02:30 -0700383
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000384 requests = _make_tryjob_schedule_requests(changelist, jobs, options,
385 patchset)
386 if not requests:
387 return
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000388
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000389 http = auth.Authenticator().authorize(httplib2.Http())
390 http.force_exception_to_status_code = True
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000391
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000392 batch_request = {'requests': requests}
393 batch_response = _call_buildbucket(http, DEFAULT_BUILDBUCKET_HOST, 'Batch',
394 batch_request)
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000395
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000396 errors = [
397 ' ' + response['error']['message']
398 for response in batch_response.get('responses', [])
399 if 'error' in response
400 ]
401 if errors:
402 raise BuildbucketResponseException(
403 'Failed to schedule builds for some bots:\n%s' % '\n'.join(errors))
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000404
405
Quinten Yearsley777660f2020-03-04 23:37:06 +0000406def _make_tryjob_schedule_requests(changelist, jobs, options, patchset):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000407 """Constructs requests for Buildbucket to trigger tryjobs."""
408 gerrit_changes = [changelist.GetGerritChange(patchset)]
409 shared_properties = {
410 'category': options.ensure_value('category', 'git_cl_try')
411 }
412 if options.ensure_value('clobber', False):
413 shared_properties['clobber'] = True
414 shared_properties.update(_get_properties_from_options(options) or {})
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000415
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000416 shared_tags = [{'key': 'user_agent', 'value': 'git_cl_try'}]
417 if options.ensure_value('retry_failed', False):
418 shared_tags.append({'key': 'retry_failed', 'value': '1'})
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000419
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000420 requests = []
421 for (project, bucket, builder) in jobs:
422 properties = shared_properties.copy()
423 if 'presubmit' in builder.lower():
424 properties['dry_run'] = 'true'
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000425
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000426 requests.append({
427 'scheduleBuild': {
428 'requestId': str(uuid.uuid4()),
429 'builder': {
430 'project': getattr(options, 'project', None) or project,
431 'bucket': bucket,
432 'builder': builder,
433 },
434 'gerritChanges': gerrit_changes,
435 'properties': properties,
436 'tags': [
437 {
438 'key': 'builder',
439 'value': builder
440 },
441 ] + shared_tags,
442 }
443 })
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000444
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000445 if options.ensure_value('revision', None):
446 remote, remote_branch = changelist.GetRemoteBranch()
447 requests[-1]['scheduleBuild']['gitilesCommit'] = {
448 'host':
449 _canonical_git_googlesource_host(gerrit_changes[0]['host']),
450 'project': gerrit_changes[0]['project'],
451 'id': options.revision,
452 'ref': GetTargetRef(remote, remote_branch, None)
453 }
Anthony Polito1a5fe232020-01-24 23:17:52 +0000454
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000455 return requests
kjellander@chromium.org44424542015-06-02 18:35:29 +0000456
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000457
Quinten Yearsley777660f2020-03-04 23:37:06 +0000458def _fetch_tryjobs(changelist, buildbucket_host, patchset=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000459 """Fetches tryjobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000460
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000461 Returns list of buildbucket.v2.Build with the try jobs for the changelist.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000462 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000463 fields = ['id', 'builder', 'status', 'createTime', 'tags']
464 request = {
465 'predicate': {
466 'gerritChanges': [changelist.GetGerritChange(patchset)],
467 },
468 'fields': ','.join('builds.*.' + field for field in fields),
469 }
tandrii221ab252016-10-06 08:12:04 -0700470
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000471 authenticator = auth.Authenticator()
472 if authenticator.has_cached_credentials():
473 http = authenticator.authorize(httplib2.Http())
474 else:
475 print('Warning: Some results might be missing because %s' %
476 # Get the message on how to login.
477 (
478 str(auth.LoginRequiredError()), ))
479 http = httplib2.Http()
480 http.force_exception_to_status_code = True
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000481
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000482 response = _call_buildbucket(http, buildbucket_host, 'SearchBuilds',
483 request)
484 return response.get('builds', [])
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000485
Edward Lemur45768512020-03-02 19:03:14 +0000486
Edward Lemur5b929a42019-10-21 17:57:39 +0000487def _fetch_latest_builds(changelist, buildbucket_host, latest_patchset=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000488 """Fetches builds from the latest patchset that has builds (within
Quinten Yearsley983111f2019-09-26 17:18:48 +0000489 the last few patchsets).
490
491 Args:
Quinten Yearsley983111f2019-09-26 17:18:48 +0000492 changelist (Changelist): The CL to fetch builds for
493 buildbucket_host (str): Buildbucket host, e.g. "cr-buildbucket.appspot.com"
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +0000494 lastest_patchset(int|NoneType): the patchset to start fetching builds from.
495 If None (default), starts with the latest available patchset.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000496 Returns:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000497 A tuple (builds, patchset) where builds is a list of buildbucket.v2.Build,
498 and patchset is the patchset number where those builds came from.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000499 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000500 assert buildbucket_host
501 assert changelist.GetIssue(), 'CL must be uploaded first'
502 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
503 if latest_patchset is None:
504 assert changelist.GetMostRecentPatchset()
505 ps = changelist.GetMostRecentPatchset()
506 else:
507 assert latest_patchset > 0, latest_patchset
508 ps = latest_patchset
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +0000509
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000510 min_ps = max(1, ps - 5)
511 while ps >= min_ps:
512 builds = _fetch_tryjobs(changelist, buildbucket_host, patchset=ps)
513 if len(builds):
514 return builds, ps
515 ps -= 1
516 return [], 0
Quinten Yearsley983111f2019-09-26 17:18:48 +0000517
518
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000519def _filter_failed_for_retry(all_builds):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000520 """Returns a list of buckets/builders that are worth retrying.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000521
522 Args:
Quinten Yearsley777660f2020-03-04 23:37:06 +0000523 all_builds (list): Builds, in the format returned by _fetch_tryjobs,
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000524 i.e. a list of buildbucket.v2.Builds which includes status and builder
525 info.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000526
527 Returns:
Edward Lemur45768512020-03-02 19:03:14 +0000528 A dict {(proj, bucket): [builders]}. This is the same format accepted by
Quinten Yearsley777660f2020-03-04 23:37:06 +0000529 _trigger_tryjobs.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000530 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000531 grouped = {}
532 for build in all_builds:
533 builder = build['builder']
534 key = (builder['project'], builder['bucket'], builder['builder'])
535 grouped.setdefault(key, []).append(build)
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000536
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000537 jobs = []
538 for (project, bucket, builder), builds in grouped.items():
539 if 'triggered' in builder:
540 print(
541 'WARNING: Not scheduling %s. Triggered bots require an initial job '
542 'from a parent. Please schedule a manual job for the parent '
543 'instead.')
544 continue
545 if any(b['status'] in ('STARTED', 'SCHEDULED') for b in builds):
546 # Don't retry if any are running.
547 continue
548 # If builder had several builds, retry only if the last one failed.
549 # This is a bit different from CQ, which would re-use *any* SUCCESS-full
550 # build, but in case of retrying failed jobs retrying a flaky one makes
551 # sense.
552 builds = sorted(builds, key=lambda b: b['createTime'])
553 if builds[-1]['status'] not in ('FAILURE', 'INFRA_FAILURE'):
554 continue
555 # Don't retry experimental build previously triggered by CQ.
556 if any(t['key'] == 'cq_experimental' and t['value'] == 'true'
557 for t in builds[-1]['tags']):
558 continue
559 jobs.append((project, bucket, builder))
Edward Lemur45768512020-03-02 19:03:14 +0000560
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000561 # Sort the jobs to make testing easier.
562 return sorted(jobs)
Quinten Yearsley983111f2019-09-26 17:18:48 +0000563
564
Quinten Yearsley777660f2020-03-04 23:37:06 +0000565def _print_tryjobs(options, builds):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000566 """Prints nicely result of _fetch_tryjobs."""
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000567 if not builds:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000568 print('No tryjobs scheduled.')
569 return
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000570
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000571 longest_builder = max(len(b['builder']['builder']) for b in builds)
572 name_fmt = '{builder:<%d}' % longest_builder
573 if options.print_master:
574 longest_bucket = max(len(b['builder']['bucket']) for b in builds)
575 name_fmt = ('{bucket:>%d} ' % longest_bucket) + name_fmt
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000576
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000577 builds_by_status = {}
578 for b in builds:
579 builds_by_status.setdefault(b['status'], []).append({
580 'id':
581 b['id'],
582 'name':
583 name_fmt.format(builder=b['builder']['builder'],
584 bucket=b['builder']['bucket']),
585 })
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000586
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000587 sort_key = lambda b: (b['name'], b['id'])
588
589 def print_builds(title, builds, fmt=None, color=None):
590 """Pop matching builds from `builds` dict and print them."""
591 if not builds:
592 return
593
594 fmt = fmt or '{name} https://ci.chromium.org/b/{id}'
595 if not options.color or color is None:
596 colorize = lambda x: x
597 else:
598 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
599
600 print(colorize(title))
601 for b in sorted(builds, key=sort_key):
602 print(' ', colorize(fmt.format(**b)))
603
604 total = len(builds)
605 print_builds('Successes:',
606 builds_by_status.pop('SUCCESS', []),
607 color=Fore.GREEN)
608 print_builds('Infra Failures:',
609 builds_by_status.pop('INFRA_FAILURE', []),
610 color=Fore.MAGENTA)
611 print_builds('Failures:',
612 builds_by_status.pop('FAILURE', []),
613 color=Fore.RED)
614 print_builds('Canceled:',
615 builds_by_status.pop('CANCELED', []),
616 fmt='{name}',
617 color=Fore.MAGENTA)
618 print_builds('Started:',
619 builds_by_status.pop('STARTED', []),
620 color=Fore.YELLOW)
621 print_builds('Scheduled:',
622 builds_by_status.pop('SCHEDULED', []),
623 fmt='{name} id={id}')
624 # The last section is just in case buildbucket API changes OR there is a
625 # bug.
626 print_builds('Other:',
627 sum(builds_by_status.values(), []),
628 fmt='{name} id={id}')
629 print('Total: %d tryjobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000630
631
Aiden Bennerc08566e2018-10-03 17:52:42 +0000632def _ComputeDiffLineRanges(files, upstream_commit):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000633 """Gets the changed line ranges for each file since upstream_commit.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000634
635 Parses a git diff on provided files and returns a dict that maps a file name
636 to an ordered list of range tuples in the form (start_line, count).
637 Ranges are in the same format as a git diff.
638 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000639 # If files is empty then diff_output will be a full diff.
640 if len(files) == 0:
641 return {}
Aiden Bennerc08566e2018-10-03 17:52:42 +0000642
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000643 # Take the git diff and find the line ranges where there are changes.
644 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
645 diff_output = RunGit(diff_cmd)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000646
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000647 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
648 # 2 capture groups
649 # 0 == fname of diff file
650 # 1 == 'diff_start,diff_count' or 'diff_start'
651 # will match each of
652 # diff --git a/foo.foo b/foo.py
653 # @@ -12,2 +14,3 @@
654 # @@ -12,2 +17 @@
655 # running re.findall on the above string with pattern will give
656 # [('foo.py', ''), ('', '14,3'), ('', '17')]
Aiden Bennerc08566e2018-10-03 17:52:42 +0000657
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000658 curr_file = None
659 line_diffs = {}
660 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
661 if match[0] != '':
662 # Will match the second filename in diff --git a/a.py b/b.py.
663 curr_file = match[0]
664 line_diffs[curr_file] = []
665 else:
666 # Matches +14,3
667 if ',' in match[1]:
668 diff_start, diff_count = match[1].split(',')
669 else:
670 # Single line changes are of the form +12 instead of +12,1.
671 diff_start = match[1]
672 diff_count = 1
Aiden Bennerc08566e2018-10-03 17:52:42 +0000673
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000674 diff_start = int(diff_start)
675 diff_count = int(diff_count)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000676
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000677 # If diff_count == 0 this is a removal we can ignore.
678 line_diffs[curr_file].append((diff_start, diff_count))
Aiden Bennerc08566e2018-10-03 17:52:42 +0000679
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000680 return line_diffs
Aiden Bennerc08566e2018-10-03 17:52:42 +0000681
682
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000683def _FindYapfConfigFile(fpath, yapf_config_cache, top_dir=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000684 """Checks if a yapf file is in any parent directory of fpath until top_dir.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000685
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000686 Recursively checks parent directories to find yapf file and if no yapf file
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000687 is found returns None. Uses yapf_config_cache as a cache for previously found
688 configs.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000689 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000690 fpath = os.path.abspath(fpath)
691 # Return result if we've already computed it.
692 if fpath in yapf_config_cache:
693 return yapf_config_cache[fpath]
Aiden Bennerc08566e2018-10-03 17:52:42 +0000694
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000695 parent_dir = os.path.dirname(fpath)
696 if os.path.isfile(fpath):
697 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000698 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000699 # Otherwise fpath is a directory
700 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
701 if os.path.isfile(yapf_file):
702 ret = yapf_file
703 elif fpath in (top_dir, parent_dir):
704 # If we're at the top level directory, or if we're at root
705 # there is no provided style.
706 ret = None
707 else:
708 # Otherwise recurse on the current directory.
709 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
710 yapf_config_cache[fpath] = ret
711 return ret
Aiden Bennerc08566e2018-10-03 17:52:42 +0000712
713
Brian Sheedyb4307d52019-12-02 19:18:17 +0000714def _GetYapfIgnorePatterns(top_dir):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000715 """Returns all patterns in the .yapfignore file.
Brian Sheedy59b06a82019-10-14 17:03:29 +0000716
717 yapf is supposed to handle the ignoring of files listed in .yapfignore itself,
718 but this functionality appears to break when explicitly passing files to
719 yapf for formatting. According to
Josip Sokcevicc39ab992020-09-24 20:09:15 +0000720 https://github.com/google/yapf/blob/HEAD/README.rst#excluding-files-from-formatting-yapfignore,
Brian Sheedy59b06a82019-10-14 17:03:29 +0000721 the .yapfignore file should be in the directory that yapf is invoked from,
722 which we assume to be the top level directory in this case.
723
724 Args:
725 top_dir: The top level directory for the repository being formatted.
726
727 Returns:
Brian Sheedyb4307d52019-12-02 19:18:17 +0000728 A set of all fnmatch patterns to be ignored.
Brian Sheedy59b06a82019-10-14 17:03:29 +0000729 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000730 yapfignore_file = os.path.join(top_dir, '.yapfignore')
731 ignore_patterns = set()
732 if not os.path.exists(yapfignore_file):
733 return ignore_patterns
Brian Sheedy59b06a82019-10-14 17:03:29 +0000734
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000735 for line in gclient_utils.FileRead(yapfignore_file).split('\n'):
736 stripped_line = line.strip()
737 # Comments and blank lines should be ignored.
738 if stripped_line.startswith('#') or stripped_line == '':
739 continue
740 ignore_patterns.add(stripped_line)
741 return ignore_patterns
Brian Sheedyb4307d52019-12-02 19:18:17 +0000742
743
744def _FilterYapfIgnoredFiles(filepaths, patterns):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000745 """Filters out any filepaths that match any of the given patterns.
Brian Sheedyb4307d52019-12-02 19:18:17 +0000746
747 Args:
748 filepaths: An iterable of strings containing filepaths to filter.
749 patterns: An iterable of strings containing fnmatch patterns to filter on.
750
751 Returns:
752 A list of strings containing all the elements of |filepaths| that did not
753 match any of the patterns in |patterns|.
754 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000755 # Not inlined so that tests can use the same implementation.
756 return [
757 f for f in filepaths
758 if not any(fnmatch.fnmatch(f, p) for p in patterns)
759 ]
Brian Sheedy59b06a82019-10-14 17:03:29 +0000760
761
Daniel Cheng66d0f152023-08-29 23:21:58 +0000762def _GetCommitCountSummary(begin_commit: str,
763 end_commit: str = "HEAD") -> Optional[str]:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000764 """Generate a summary of the number of commits in (begin_commit, end_commit).
Daniel Cheng66d0f152023-08-29 23:21:58 +0000765
766 Returns a string containing the summary, or None if the range is empty.
767 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000768 count = int(
769 RunGitSilent(['rev-list', '--count', f'{begin_commit}..{end_commit}']))
Daniel Cheng66d0f152023-08-29 23:21:58 +0000770
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000771 if not count:
772 return None
Daniel Cheng66d0f152023-08-29 23:21:58 +0000773
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000774 return f'{count} commit{"s"[:count!=1]}'
Daniel Cheng66d0f152023-08-29 23:21:58 +0000775
776
Aaron Gable13101a62018-02-09 13:20:41 -0800777def print_stats(args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000778 """Prints statistics about the change to the user."""
779 # --no-ext-diff is broken in some versions of Git, so try to work around
780 # this by overriding the environment (but there is still a problem if the
781 # git config key "diff.external" is used).
782 env = GetNoGitPagerEnv()
783 if 'GIT_EXTERNAL_DIFF' in env:
784 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000785
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000786 return subprocess2.call(
787 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
788 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000789
790
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000791class BuildbucketResponseException(Exception):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000792 pass
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000793
794
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000795class Settings(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000796 def __init__(self):
797 self.cc = None
798 self.root = None
799 self.tree_status_url = None
800 self.viewvc_url = None
801 self.updated = False
802 self.is_gerrit = None
803 self.squash_gerrit_uploads = None
804 self.gerrit_skip_ensure_authenticated = None
805 self.git_editor = None
806 self.format_full_by_default = None
807 self.is_status_commit_order_by_date = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000808
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000809 def _LazyUpdateIfNeeded(self):
810 """Updates the settings from a codereview.settings file, if available."""
811 if self.updated:
812 return
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000813
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000814 # The only value that actually changes the behavior is
815 # autoupdate = "false". Everything else means "true".
816 autoupdate = (scm.GIT.GetConfig(self.GetRoot(), 'rietveld.autoupdate',
817 '').lower())
Edward Lemur26964072020-02-19 19:18:51 +0000818
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000819 cr_settings_file = FindCodereviewSettingsFile()
820 if autoupdate != 'false' and cr_settings_file:
821 LoadCodereviewSettingsFromFile(cr_settings_file)
822 cr_settings_file.close()
Edward Lemur26964072020-02-19 19:18:51 +0000823
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000824 self.updated = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000825
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000826 @staticmethod
827 def GetRelativeRoot():
828 return scm.GIT.GetCheckoutRoot('.')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000829
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000830 def GetRoot(self):
831 if self.root is None:
832 self.root = os.path.abspath(self.GetRelativeRoot())
833 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000834
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000835 def GetTreeStatusUrl(self, error_ok=False):
836 if not self.tree_status_url:
837 self.tree_status_url = self._GetConfig('rietveld.tree-status-url')
838 if self.tree_status_url is None and not error_ok:
839 DieWithError(
840 'You must configure your tree status URL by running '
841 '"git cl config".')
842 return self.tree_status_url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000843
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000844 def GetViewVCUrl(self):
845 if not self.viewvc_url:
846 self.viewvc_url = self._GetConfig('rietveld.viewvc-url')
847 return self.viewvc_url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000848
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000849 def GetBugPrefix(self):
850 return self._GetConfig('rietveld.bug-prefix')
rmistry@google.com78948ed2015-07-08 23:09:57 +0000851
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000852 def GetRunPostUploadHook(self):
853 run_post_upload_hook = self._GetConfig('rietveld.run-post-upload-hook')
854 return run_post_upload_hook == "True"
rmistry@google.com5626a922015-02-26 14:03:30 +0000855
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000856 def GetDefaultCCList(self):
857 return self._GetConfig('rietveld.cc')
Joanna Wangc8f23e22023-01-19 21:18:10 +0000858
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000859 def GetSquashGerritUploads(self):
860 """Returns True if uploads to Gerrit should be squashed by default."""
861 if self.squash_gerrit_uploads is None:
862 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
863 if self.squash_gerrit_uploads is None:
864 # Default is squash now (http://crbug.com/611892#c23).
865 self.squash_gerrit_uploads = self._GetConfig(
866 'gerrit.squash-uploads').lower() != 'false'
867 return self.squash_gerrit_uploads
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000868
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000869 def GetSquashGerritUploadsOverride(self):
870 """Return True or False if codereview.settings should be overridden.
Edward Lesmes4de54132020-05-05 19:41:33 +0000871
872 Returns None if no override has been defined.
873 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000874 # See also http://crbug.com/611892#c23
875 result = self._GetConfig('gerrit.override-squash-uploads').lower()
876 if result == 'true':
877 return True
878 if result == 'false':
879 return False
880 return None
Edward Lesmes4de54132020-05-05 19:41:33 +0000881
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000882 def GetIsGerrit(self):
883 """Return True if gerrit.host is set."""
884 if self.is_gerrit is None:
885 self.is_gerrit = bool(self._GetConfig('gerrit.host', False))
886 return self.is_gerrit
Aleksey Khoroshilov35ef5ad2022-06-03 18:29:25 +0000887
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000888 def GetGerritSkipEnsureAuthenticated(self):
889 """Return True if EnsureAuthenticated should not be done for Gerrit
tandrii@chromium.org28253532016-04-14 13:46:56 +0000890 uploads."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000891 if self.gerrit_skip_ensure_authenticated is None:
892 self.gerrit_skip_ensure_authenticated = self._GetConfig(
893 'gerrit.skip-ensure-authenticated').lower() == 'true'
894 return self.gerrit_skip_ensure_authenticated
tandrii@chromium.org28253532016-04-14 13:46:56 +0000895
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000896 def GetGitEditor(self):
897 """Returns the editor specified in the git config, or None if none is."""
898 if self.git_editor is None:
899 # Git requires single quotes for paths with spaces. We need to
900 # replace them with double quotes for Windows to treat such paths as
901 # a single path.
902 self.git_editor = self._GetConfig('core.editor').replace('\'', '"')
903 return self.git_editor or None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000904
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000905 def GetLintRegex(self):
906 return self._GetConfig('rietveld.cpplint-regex', DEFAULT_LINT_REGEX)
thestig@chromium.org44202a22014-03-11 19:22:18 +0000907
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000908 def GetLintIgnoreRegex(self):
909 return self._GetConfig('rietveld.cpplint-ignore-regex',
910 DEFAULT_LINT_IGNORE_REGEX)
thestig@chromium.org44202a22014-03-11 19:22:18 +0000911
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000912 def GetFormatFullByDefault(self):
913 if self.format_full_by_default is None:
914 self._LazyUpdateIfNeeded()
915 result = (RunGit(
916 ['config', '--bool', 'rietveld.format-full-by-default'],
917 error_ok=True).strip())
918 self.format_full_by_default = (result == 'true')
919 return self.format_full_by_default
Jamie Madilldc4d19e2019-10-24 21:50:02 +0000920
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000921 def IsStatusCommitOrderByDate(self):
922 if self.is_status_commit_order_by_date is None:
923 result = (RunGit(['config', '--bool', 'cl.date-order'],
924 error_ok=True).strip())
925 self.is_status_commit_order_by_date = (result == 'true')
926 return self.is_status_commit_order_by_date
Sigurd Schneider1bfda8e2021-06-30 14:46:25 +0000927
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000928 def _GetConfig(self, key, default=''):
929 self._LazyUpdateIfNeeded()
930 return scm.GIT.GetConfig(self.GetRoot(), key, default)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000931
932
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000933class _CQState(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000934 """Enum for states of CL with respect to CQ."""
935 NONE = 'none'
936 DRY_RUN = 'dry_run'
937 COMMIT = 'commit'
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000938
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000939 ALL_STATES = [NONE, DRY_RUN, COMMIT]
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000940
941
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000942class _ParsedIssueNumberArgument(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000943 def __init__(self, issue=None, patchset=None, hostname=None):
944 self.issue = issue
945 self.patchset = patchset
946 self.hostname = hostname
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000947
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000948 @property
949 def valid(self):
950 return self.issue is not None
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000951
952
Edward Lemurf38bc172019-09-03 21:02:13 +0000953def ParseIssueNumberArgument(arg):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000954 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
955 fail_result = _ParsedIssueNumberArgument()
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000956
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000957 if isinstance(arg, int):
958 return _ParsedIssueNumberArgument(issue=arg)
959 if not isinstance(arg, str):
960 return fail_result
Edward Lemur678a6842019-10-03 22:25:05 +0000961
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000962 if arg.isdigit():
963 return _ParsedIssueNumberArgument(issue=int(arg))
Aaron Gableaee6c852017-06-26 12:49:01 -0700964
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000965 url = gclient_utils.UpgradeToHttps(arg)
966 if not url.startswith('http'):
967 return fail_result
968 for gerrit_url, short_url in _KNOWN_GERRIT_TO_SHORT_URLS.items():
969 if url.startswith(short_url):
970 url = gerrit_url + url[len(short_url):]
971 break
Andrii Shyshkalov8aebb602020-04-16 22:10:27 +0000972
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000973 try:
974 parsed_url = urllib.parse.urlparse(url)
975 except ValueError:
976 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +0200977
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000978 # If "https://" was automatically added, fail if `arg` looks unlikely to be
979 # a URL.
980 if not arg.startswith('http') and '.' not in parsed_url.netloc:
981 return fail_result
Alex Turner30ae6372022-01-04 02:32:52 +0000982
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000983 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
984 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
985 # Short urls like https://domain/<issue_number> can be used, but don't allow
986 # specifying the patchset (you'd 404), but we allow that here.
987 if parsed_url.path == '/':
988 part = parsed_url.fragment
989 else:
990 part = parsed_url.path
Edward Lemur678a6842019-10-03 22:25:05 +0000991
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000992 match = re.match(r'(/c(/.*/\+)?)?/(?P<issue>\d+)(/(?P<patchset>\d+)?/?)?$',
993 part)
994 if not match:
995 return fail_result
Edward Lemur678a6842019-10-03 22:25:05 +0000996
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000997 issue = int(match.group('issue'))
998 patchset = match.group('patchset')
999 return _ParsedIssueNumberArgument(
1000 issue=issue,
1001 patchset=int(patchset) if patchset else None,
1002 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001003
1004
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00001005def _create_description_from_log(args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001006 """Pulls out the commit log to use as a base for the CL description."""
1007 log_args = []
1008 if len(args) == 1 and args[0] == None:
1009 # Handle the case where None is passed as the branch.
1010 return ''
1011 if len(args) == 1 and not args[0].endswith('.'):
1012 log_args = [args[0] + '..']
1013 elif len(args) == 1 and args[0].endswith('...'):
1014 log_args = [args[0][:-1]]
1015 elif len(args) == 2:
1016 log_args = [args[0] + '..' + args[1]]
1017 else:
1018 log_args = args[:] # Hope for the best!
1019 return RunGit(['log', '--pretty=format:%B%n'] + log_args)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00001020
1021
Aaron Gablea45ee112016-11-22 15:14:38 -08001022class GerritChangeNotExists(Exception):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001023 def __init__(self, issue, url):
1024 self.issue = issue
1025 self.url = url
1026 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001027
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001028 def __str__(self):
1029 return 'change %s at %s does not exist or you have no access to it' % (
1030 self.issue, self.url)
tandriic2405f52016-10-10 08:13:15 -07001031
1032
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001033_CommentSummary = collections.namedtuple(
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001034 '_CommentSummary',
1035 [
1036 'date',
1037 'message',
1038 'sender',
1039 'autogenerated',
1040 # TODO(tandrii): these two aren't known in Gerrit.
1041 'approval',
1042 'disapproval'
1043 ])
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001044
Joanna Wang6215dd02023-02-07 15:58:03 +00001045# TODO(b/265929888): Change `parent` to `pushed_commit_base`.
Joanna Wange8523912023-01-21 02:05:40 +00001046_NewUpload = collections.namedtuple('NewUpload', [
Joanna Wang40497912023-01-24 21:18:16 +00001047 'reviewers', 'ccs', 'commit_to_push', 'new_last_uploaded_commit', 'parent',
Joanna Wang7603f042023-03-01 22:17:36 +00001048 'change_desc', 'prev_patchset'
Joanna Wange8523912023-01-21 02:05:40 +00001049])
1050
1051
Daniel Chengabf48472023-08-30 15:45:13 +00001052class ChangeDescription(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001053 """Contains a parsed form of the change description."""
1054 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
1055 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
1056 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
1057 FIXED_LINE = r'^[ \t]*Fixed[ \t]*:[ \t]*(.*?)[ \t]*$'
1058 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
1059 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
1060 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
1061 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):($|[^:])'
1062 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
Daniel Chengabf48472023-08-30 15:45:13 +00001063
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001064 def __init__(self, description, bug=None, fixed=None):
1065 self._description_lines = (description or '').strip().splitlines()
1066 if bug:
1067 regexp = re.compile(self.BUG_LINE)
1068 prefix = settings.GetBugPrefix()
1069 if not any(
1070 (regexp.match(line) for line in self._description_lines)):
1071 values = list(_get_bug_line_values(prefix, bug))
1072 self.append_footer('Bug: %s' % ', '.join(values))
1073 if fixed:
1074 regexp = re.compile(self.FIXED_LINE)
1075 prefix = settings.GetBugPrefix()
1076 if not any(
1077 (regexp.match(line) for line in self._description_lines)):
1078 values = list(_get_bug_line_values(prefix, fixed))
1079 self.append_footer('Fixed: %s' % ', '.join(values))
Daniel Chengabf48472023-08-30 15:45:13 +00001080
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001081 @property # www.logilab.org/ticket/89786
1082 def description(self): # pylint: disable=method-hidden
1083 return '\n'.join(self._description_lines)
Daniel Chengabf48472023-08-30 15:45:13 +00001084
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001085 def set_description(self, desc):
1086 if isinstance(desc, str):
1087 lines = desc.splitlines()
1088 else:
1089 lines = [line.rstrip() for line in desc]
1090 while lines and not lines[0]:
1091 lines.pop(0)
1092 while lines and not lines[-1]:
1093 lines.pop(-1)
1094 self._description_lines = lines
Daniel Chengabf48472023-08-30 15:45:13 +00001095
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001096 def ensure_change_id(self, change_id):
1097 description = self.description
1098 footer_change_ids = git_footers.get_footer_change_id(description)
1099 # Make sure that the Change-Id in the description matches the given one.
1100 if footer_change_ids != [change_id]:
1101 if footer_change_ids:
1102 # Remove any existing Change-Id footers since they don't match
1103 # the expected change_id footer.
1104 description = git_footers.remove_footer(description,
1105 'Change-Id')
1106 print(
1107 'WARNING: Change-Id has been set to %s. Use `git cl issue 0` '
1108 'if you want to set a new one.')
1109 # Add the expected Change-Id footer.
1110 description = git_footers.add_footer_change_id(
1111 description, change_id)
1112 self.set_description(description)
Daniel Chengabf48472023-08-30 15:45:13 +00001113
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001114 def update_reviewers(self, reviewers):
1115 """Rewrites the R= line(s) as a single line each.
Daniel Chengabf48472023-08-30 15:45:13 +00001116
1117 Args:
1118 reviewers (list(str)) - list of additional emails to use for reviewers.
1119 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001120 if not reviewers:
1121 return
Daniel Chengabf48472023-08-30 15:45:13 +00001122
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001123 reviewers = set(reviewers)
Daniel Chengabf48472023-08-30 15:45:13 +00001124
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001125 # Get the set of R= lines and remove them from the description.
1126 regexp = re.compile(self.R_LINE)
1127 matches = [regexp.match(line) for line in self._description_lines]
1128 new_desc = [
1129 l for i, l in enumerate(self._description_lines) if not matches[i]
1130 ]
1131 self.set_description(new_desc)
Daniel Chengabf48472023-08-30 15:45:13 +00001132
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001133 # Construct new unified R= lines.
Daniel Chengabf48472023-08-30 15:45:13 +00001134
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001135 # First, update reviewers with names from the R= lines (if any).
1136 for match in matches:
1137 if not match:
1138 continue
1139 reviewers.update(cleanup_list([match.group(2).strip()]))
Daniel Chengabf48472023-08-30 15:45:13 +00001140
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001141 new_r_line = 'R=' + ', '.join(sorted(reviewers))
Daniel Chengabf48472023-08-30 15:45:13 +00001142
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001143 # Put the new lines in the description where the old first R= line was.
1144 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1145 if 0 <= line_loc < len(self._description_lines):
1146 self._description_lines.insert(line_loc, new_r_line)
1147 else:
1148 self.append_footer(new_r_line)
Daniel Chengabf48472023-08-30 15:45:13 +00001149
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001150 def set_preserve_tryjobs(self):
1151 """Ensures description footer contains 'Cq-Do-Not-Cancel-Tryjobs: true'."""
1152 footers = git_footers.parse_footers(self.description)
1153 for v in footers.get('Cq-Do-Not-Cancel-Tryjobs', []):
1154 if v.lower() == 'true':
1155 return
1156 self.append_footer('Cq-Do-Not-Cancel-Tryjobs: true')
Daniel Chengabf48472023-08-30 15:45:13 +00001157
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001158 def prompt(self):
1159 """Asks the user to update the description."""
1160 self.set_description([
1161 '# Enter a description of the change.',
1162 '# This will be displayed on the codereview site.',
1163 '# The first line will also be used as the subject of the review.',
1164 '#--------------------This line is 72 characters long'
1165 '--------------------',
1166 ] + self._description_lines)
1167 bug_regexp = re.compile(self.BUG_LINE)
1168 fixed_regexp = re.compile(self.FIXED_LINE)
1169 prefix = settings.GetBugPrefix()
1170 has_issue = lambda l: bug_regexp.match(l) or fixed_regexp.match(l)
Daniel Chengabf48472023-08-30 15:45:13 +00001171
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001172 if not any((has_issue(line) for line in self._description_lines)):
1173 self.append_footer('Bug: %s' % prefix)
Daniel Chengabf48472023-08-30 15:45:13 +00001174
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001175 print('Waiting for editor...')
1176 content = gclient_utils.RunEditor(self.description,
1177 True,
1178 git_editor=settings.GetGitEditor())
1179 if not content:
1180 DieWithError('Running editor failed')
1181 lines = content.splitlines()
Daniel Chengabf48472023-08-30 15:45:13 +00001182
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001183 # Strip off comments and default inserted "Bug:" line.
1184 clean_lines = [
1185 line.rstrip() for line in lines
1186 if not (line.startswith('#') or line.rstrip() == "Bug:"
1187 or line.rstrip() == "Bug: " + prefix)
1188 ]
1189 if not clean_lines:
1190 DieWithError('No CL description, aborting')
1191 self.set_description(clean_lines)
Daniel Chengabf48472023-08-30 15:45:13 +00001192
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001193 def append_footer(self, line):
1194 """Adds a footer line to the description.
Daniel Chengabf48472023-08-30 15:45:13 +00001195
1196 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
1197 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
1198 that Gerrit footers are always at the end.
1199 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001200 parsed_footer_line = git_footers.parse_footer(line)
1201 if parsed_footer_line:
1202 # Line is a gerrit footer in the form: Footer-Key: any value.
1203 # Thus, must be appended observing Gerrit footer rules.
1204 self.set_description(
1205 git_footers.add_footer(self.description,
1206 key=parsed_footer_line[0],
1207 value=parsed_footer_line[1]))
1208 return
Daniel Chengabf48472023-08-30 15:45:13 +00001209
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001210 if not self._description_lines:
1211 self._description_lines.append(line)
1212 return
Daniel Chengabf48472023-08-30 15:45:13 +00001213
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001214 top_lines, gerrit_footers, _ = git_footers.split_footers(
1215 self.description)
1216 if gerrit_footers:
1217 # git_footers.split_footers ensures that there is an empty line
1218 # before actual (gerrit) footers, if any. We have to keep it that
1219 # way.
1220 assert top_lines and top_lines[-1] == ''
1221 top_lines, separator = top_lines[:-1], top_lines[-1:]
1222 else:
1223 separator = [
1224 ] # No need for separator if there are no gerrit_footers.
Daniel Chengabf48472023-08-30 15:45:13 +00001225
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001226 prev_line = top_lines[-1] if top_lines else ''
1227 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line)
1228 or not presubmit_support.Change.TAG_LINE_RE.match(line)):
1229 top_lines.append('')
1230 top_lines.append(line)
1231 self._description_lines = top_lines + separator + gerrit_footers
Daniel Chengabf48472023-08-30 15:45:13 +00001232
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001233 def get_reviewers(self, tbr_only=False):
1234 """Retrieves the list of reviewers."""
1235 matches = [
1236 re.match(self.R_LINE, line) for line in self._description_lines
1237 ]
1238 reviewers = [
1239 match.group(2).strip() for match in matches
1240 if match and (not tbr_only or match.group(1).upper() == 'TBR')
1241 ]
1242 return cleanup_list(reviewers)
Daniel Chengabf48472023-08-30 15:45:13 +00001243
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001244 def get_cced(self):
1245 """Retrieves the list of reviewers."""
1246 matches = [
1247 re.match(self.CC_LINE, line) for line in self._description_lines
1248 ]
1249 cced = [match.group(2).strip() for match in matches if match]
1250 return cleanup_list(cced)
Daniel Chengabf48472023-08-30 15:45:13 +00001251
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001252 def get_hash_tags(self):
1253 """Extracts and sanitizes a list of Gerrit hashtags."""
1254 subject = (self._description_lines or ('', ))[0]
1255 subject = re.sub(self.STRIP_HASH_TAG_PREFIX,
1256 '',
1257 subject,
1258 flags=re.IGNORECASE)
Daniel Chengabf48472023-08-30 15:45:13 +00001259
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001260 tags = []
1261 start = 0
1262 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
1263 while True:
1264 m = bracket_exp.match(subject, start)
1265 if not m:
1266 break
1267 tags.append(self.sanitize_hash_tag(m.group(1)))
1268 start = m.end()
Daniel Chengabf48472023-08-30 15:45:13 +00001269
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001270 if not tags:
1271 # Try "Tag: " prefix.
1272 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
1273 if m:
1274 tags.append(self.sanitize_hash_tag(m.group(1)))
1275 return tags
Daniel Chengabf48472023-08-30 15:45:13 +00001276
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001277 @classmethod
1278 def sanitize_hash_tag(cls, tag):
1279 """Returns a sanitized Gerrit hash tag.
Daniel Chengabf48472023-08-30 15:45:13 +00001280
1281 A sanitized hashtag can be used as a git push refspec parameter value.
1282 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001283 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
Daniel Chengabf48472023-08-30 15:45:13 +00001284
1285
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001286class Changelist(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001287 """Changelist works with one changelist in local branch.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001288
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001289 Notes:
1290 * Not safe for concurrent multi-{thread,process} use.
1291 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001292 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001293 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001294 def __init__(self,
1295 branchref=None,
1296 issue=None,
1297 codereview_host=None,
1298 commit_date=None):
1299 """Create a new ChangeList instance.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001300
Edward Lemurf38bc172019-09-03 21:02:13 +00001301 **kwargs will be passed directly to Gerrit implementation.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001302 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001303 # Poke settings so we get the "configure your server" message if
1304 # necessary.
1305 global settings
1306 if not settings:
1307 # Happens when git_cl.py is used as a utility library.
1308 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001309
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001310 self.branchref = branchref
1311 if self.branchref:
1312 assert branchref.startswith('refs/heads/')
1313 self.branch = scm.GIT.ShortBranchName(self.branchref)
1314 else:
1315 self.branch = None
1316 self.commit_date = commit_date
1317 self.upstream_branch = None
1318 self.lookedup_issue = False
1319 self.issue = issue or None
1320 self.description = None
1321 self.lookedup_patchset = False
1322 self.patchset = None
1323 self.cc = None
1324 self.more_cc = []
1325 self._remote = None
1326 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001327
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001328 # Lazily cached values.
1329 self._gerrit_host = None # e.g. chromium-review.googlesource.com
1330 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
1331 self._owners_client = None
1332 # Map from change number (issue) to its detail cache.
1333 self._detail_cache = {}
Edward Lemur125d60a2019-09-13 18:25:41 +00001334
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001335 if codereview_host is not None:
1336 assert not codereview_host.startswith('https://'), codereview_host
1337 self._gerrit_host = codereview_host
1338 self._gerrit_server = 'https://%s' % codereview_host
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001339
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001340 @property
1341 def owners_client(self):
1342 if self._owners_client is None:
1343 remote, remote_branch = self.GetRemoteBranch()
1344 branch = GetTargetRef(remote, remote_branch, None)
1345 self._owners_client = owners_client.GetCodeOwnersClient(
1346 host=self.GetGerritHost(),
1347 project=self.GetGerritProject(),
1348 branch=branch)
1349 return self._owners_client
Edward Lesmese1576912021-02-16 21:53:34 +00001350
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001351 def GetCCList(self):
1352 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001353
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001354 The return value is a string suitable for passing to git cl with the --cc
1355 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001356 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001357 if self.cc is None:
1358 base_cc = settings.GetDefaultCCList()
1359 more_cc = ','.join(self.more_cc)
1360 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1361 return self.cc
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001362
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001363 def ExtendCC(self, more_cc):
1364 """Extends the list of users to cc on this CL based on the changed files."""
1365 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001366
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001367 def GetCommitDate(self):
1368 """Returns the commit date as provided in the constructor"""
1369 return self.commit_date
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00001370
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001371 def GetBranch(self):
1372 """Returns the short branch name, e.g. 'main'."""
1373 if not self.branch:
1374 branchref = scm.GIT.GetBranchRef(settings.GetRoot())
1375 if not branchref:
1376 return None
1377 self.branchref = branchref
1378 self.branch = scm.GIT.ShortBranchName(self.branchref)
1379 return self.branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001380
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001381 def GetBranchRef(self):
1382 """Returns the full branch name, e.g. 'refs/heads/main'."""
1383 self.GetBranch() # Poke the lazy loader.
1384 return self.branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001385
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001386 def _GitGetBranchConfigValue(self, key, default=None):
1387 return scm.GIT.GetBranchConfig(settings.GetRoot(), self.GetBranch(),
1388 key, default)
tandrii5d48c322016-08-18 16:19:37 -07001389
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001390 def _GitSetBranchConfigValue(self, key, value):
1391 action = 'set %s to %r' % (key, value)
1392 if not value:
1393 action = 'unset %s' % key
1394 assert self.GetBranch(), 'a branch is needed to ' + action
1395 return scm.GIT.SetBranchConfig(settings.GetRoot(), self.GetBranch(),
1396 key, value)
tandrii5d48c322016-08-18 16:19:37 -07001397
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001398 @staticmethod
1399 def FetchUpstreamTuple(branch):
1400 """Returns a tuple containing remote and remote ref,
Josip Sokcevicc39ab992020-09-24 20:09:15 +00001401 e.g. 'origin', 'refs/heads/main'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001402 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001403 remote, upstream_branch = scm.GIT.FetchUpstreamTuple(
1404 settings.GetRoot(), branch)
1405 if not remote or not upstream_branch:
1406 DieWithError(
1407 'Unable to determine default branch to diff against.\n'
1408 'Verify this branch is set up to track another \n'
1409 '(via the --track argument to "git checkout -b ..."). \n'
1410 'or pass complete "git diff"-style arguments if supported, like\n'
1411 ' git cl upload origin/main\n')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001412
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001413 return remote, upstream_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001414
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001415 def GetCommonAncestorWithUpstream(self):
1416 upstream_branch = self.GetUpstreamBranch()
1417 if not scm.GIT.IsValidRevision(settings.GetRoot(), upstream_branch):
1418 DieWithError(
1419 'The upstream for the current branch (%s) does not exist '
1420 'anymore.\nPlease fix it and try again.' % self.GetBranch())
1421 return git_common.get_or_create_merge_base(self.GetBranch(),
1422 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001423
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001424 def GetUpstreamBranch(self):
1425 if self.upstream_branch is None:
1426 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
1427 if remote != '.':
1428 upstream_branch = upstream_branch.replace(
1429 'refs/heads/', 'refs/remotes/%s/' % remote)
1430 upstream_branch = upstream_branch.replace(
1431 'refs/branch-heads/', 'refs/remotes/branch-heads/')
1432 self.upstream_branch = upstream_branch
1433 return self.upstream_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001434
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001435 def GetRemoteBranch(self):
1436 if not self._remote:
1437 remote, branch = None, self.GetBranch()
1438 seen_branches = set()
1439 while branch not in seen_branches:
1440 seen_branches.add(branch)
1441 remote, branch = self.FetchUpstreamTuple(branch)
1442 branch = scm.GIT.ShortBranchName(branch)
1443 if remote != '.' or branch.startswith('refs/remotes'):
1444 break
1445 else:
1446 remotes = RunGit(['remote'], error_ok=True).split()
1447 if len(remotes) == 1:
1448 remote, = remotes
1449 elif 'origin' in remotes:
1450 remote = 'origin'
1451 logging.warning(
1452 'Could not determine which remote this change is '
1453 'associated with, so defaulting to "%s".' %
1454 self._remote)
1455 else:
1456 logging.warning(
1457 'Could not determine which remote this change is '
1458 'associated with.')
1459 branch = 'HEAD'
1460 if branch.startswith('refs/remotes'):
1461 self._remote = (remote, branch)
1462 elif branch.startswith('refs/branch-heads/'):
1463 self._remote = (remote, branch.replace('refs/',
1464 'refs/remotes/'))
1465 else:
1466 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
1467 return self._remote
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001468
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001469 def GetRemoteUrl(self) -> Optional[str]:
1470 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001471
1472 Returns None if there is no remote.
1473 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001474 is_cached, value = self._cached_remote_url
1475 if is_cached:
1476 return value
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001477
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001478 remote, _ = self.GetRemoteBranch()
1479 url = scm.GIT.GetConfig(settings.GetRoot(), 'remote.%s.url' % remote,
1480 '')
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001481
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001482 # Check if the remote url can be parsed as an URL.
1483 host = urllib.parse.urlparse(url).netloc
1484 if host:
1485 self._cached_remote_url = (True, url)
1486 return url
Edward Lemur298f2cf2019-02-22 21:40:39 +00001487
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001488 # If it cannot be parsed as an url, assume it is a local directory,
1489 # probably a git cache.
1490 logging.warning(
1491 '"%s" doesn\'t appear to point to a git host. '
1492 'Interpreting it as a local directory.', url)
1493 if not os.path.isdir(url):
1494 logging.error(
1495 'Remote "%(remote)s" for branch "%(branch)s" points to "%(url)s", '
1496 'but it doesn\'t exist.', {
1497 'remote': remote,
1498 'branch': self.GetBranch(),
1499 'url': url
1500 })
1501 return None
Edward Lemur298f2cf2019-02-22 21:40:39 +00001502
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001503 cache_path = url
1504 url = scm.GIT.GetConfig(url, 'remote.%s.url' % remote, '')
Edward Lemur298f2cf2019-02-22 21:40:39 +00001505
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001506 host = urllib.parse.urlparse(url).netloc
1507 if not host:
1508 logging.error(
1509 'Remote "%(remote)s" for branch "%(branch)s" points to '
1510 '"%(cache_path)s", but it is misconfigured.\n'
1511 '"%(cache_path)s" must be a git repo and must have a remote named '
1512 '"%(remote)s" pointing to the git host.', {
1513 'remote': remote,
1514 'cache_path': cache_path,
1515 'branch': self.GetBranch()
1516 })
1517 return None
Edward Lemur298f2cf2019-02-22 21:40:39 +00001518
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001519 self._cached_remote_url = (True, url)
1520 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001521
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001522 def GetIssue(self):
1523 """Returns the issue number as a int or None if not set."""
1524 if self.issue is None and not self.lookedup_issue:
1525 if self.GetBranch():
1526 self.issue = self._GitGetBranchConfigValue(ISSUE_CONFIG_KEY)
1527 if self.issue is not None:
1528 self.issue = int(self.issue)
1529 self.lookedup_issue = True
1530 return self.issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001531
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001532 def GetIssueURL(self, short=False):
1533 """Get the URL for a particular issue."""
1534 issue = self.GetIssue()
1535 if not issue:
1536 return None
1537 server = self.GetCodereviewServer()
1538 if short:
1539 server = _KNOWN_GERRIT_TO_SHORT_URLS.get(server, server)
1540 return '%s/%s' % (server, issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001541
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001542 def FetchDescription(self, pretty=False):
1543 assert self.GetIssue(), 'issue is required to query Gerrit'
Edward Lemur6c6827c2020-02-06 21:15:18 +00001544
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001545 if self.description is None:
1546 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
1547 current_rev = data['current_revision']
1548 self.description = data['revisions'][current_rev]['commit'][
1549 'message']
Edward Lemur6c6827c2020-02-06 21:15:18 +00001550
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001551 if not pretty:
1552 return self.description
Edward Lemur6c6827c2020-02-06 21:15:18 +00001553
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001554 # Set width to 72 columns + 2 space indent.
1555 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
1556 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1557 lines = self.description.splitlines()
1558 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001559
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001560 def GetPatchset(self):
1561 """Returns the patchset number as a int or None if not set."""
1562 if self.patchset is None and not self.lookedup_patchset:
1563 if self.GetBranch():
1564 self.patchset = self._GitGetBranchConfigValue(
1565 PATCHSET_CONFIG_KEY)
1566 if self.patchset is not None:
1567 self.patchset = int(self.patchset)
1568 self.lookedup_patchset = True
1569 return self.patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001570
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001571 def GetAuthor(self):
1572 return scm.GIT.GetConfig(settings.GetRoot(), 'user.email')
Edward Lemur9aa1a962020-02-25 00:58:38 +00001573
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001574 def SetPatchset(self, patchset):
1575 """Set this branch's patchset. If patchset=0, clears the patchset."""
1576 assert self.GetBranch()
1577 if not patchset:
1578 self.patchset = None
1579 else:
1580 self.patchset = int(patchset)
1581 self._GitSetBranchConfigValue(PATCHSET_CONFIG_KEY, str(self.patchset))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001582
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001583 def SetIssue(self, issue=None):
1584 """Set this branch's issue. If issue isn't given, clears the issue."""
1585 assert self.GetBranch()
1586 if issue:
1587 issue = int(issue)
1588 self._GitSetBranchConfigValue(ISSUE_CONFIG_KEY, str(issue))
1589 self.issue = issue
1590 codereview_server = self.GetCodereviewServer()
1591 if codereview_server:
1592 self._GitSetBranchConfigValue(CODEREVIEW_SERVER_CONFIG_KEY,
1593 codereview_server)
1594 else:
1595 # Reset all of these just to be clean.
1596 reset_suffixes = [
1597 LAST_UPLOAD_HASH_CONFIG_KEY,
1598 ISSUE_CONFIG_KEY,
1599 PATCHSET_CONFIG_KEY,
1600 CODEREVIEW_SERVER_CONFIG_KEY,
1601 GERRIT_SQUASH_HASH_CONFIG_KEY,
1602 ]
1603 for prop in reset_suffixes:
1604 try:
1605 self._GitSetBranchConfigValue(prop, None)
1606 except subprocess2.CalledProcessError:
1607 pass
1608 msg = RunGit(['log', '-1', '--format=%B']).strip()
1609 if msg and git_footers.get_footer_change_id(msg):
1610 print(
1611 'WARNING: The change patched into this branch has a Change-Id. '
1612 'Removing it.')
1613 RunGit([
1614 'commit', '--amend', '-m',
1615 git_footers.remove_footer(msg, 'Change-Id')
1616 ])
1617 self.lookedup_issue = True
1618 self.issue = None
1619 self.patchset = None
1620
1621 def GetAffectedFiles(self,
1622 upstream: str,
1623 end_commit: Optional[str] = None) -> Sequence[str]:
1624 """Returns the list of affected files for the given commit range."""
Edward Lemur85153282020-02-14 22:06:29 +00001625 try:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001626 return [
1627 f for _, f in scm.GIT.CaptureStatus(
1628 settings.GetRoot(), upstream, end_commit=end_commit)
1629 ]
Edward Lemur85153282020-02-14 22:06:29 +00001630 except subprocess2.CalledProcessError:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001631 DieWithError(
1632 ('\nFailed to diff against upstream branch %s\n\n'
1633 'This branch probably doesn\'t exist anymore. To reset the\n'
1634 'tracking branch, please run\n'
1635 ' git branch --set-upstream-to origin/main %s\n'
1636 'or replace origin/main with the relevant branch') %
1637 (upstream, self.GetBranch()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001638
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001639 def UpdateDescription(self, description, force=False):
1640 assert self.GetIssue(), 'issue is required to update description'
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001641
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001642 if gerrit_util.HasPendingChangeEdit(self.GetGerritHost(),
1643 self._GerritChangeIdentifier()):
1644 if not force:
1645 confirm_or_exit(
1646 'The description cannot be modified while the issue has a pending '
1647 'unpublished edit. Either publish the edit in the Gerrit web UI '
1648 'or delete it.\n\n',
1649 action='delete the unpublished edit')
Edward Lemur6c6827c2020-02-06 21:15:18 +00001650
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001651 gerrit_util.DeletePendingChangeEdit(self.GetGerritHost(),
1652 self._GerritChangeIdentifier())
1653 gerrit_util.SetCommitMessage(self.GetGerritHost(),
1654 self._GerritChangeIdentifier(),
1655 description,
1656 notify='NONE')
Edward Lemur6c6827c2020-02-06 21:15:18 +00001657
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001658 self.description = description
Edward Lemur6c6827c2020-02-06 21:15:18 +00001659
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001660 def _GetCommonPresubmitArgs(self, verbose, upstream):
1661 args = [
1662 '--root',
1663 settings.GetRoot(),
1664 '--upstream',
1665 upstream,
1666 ]
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001667
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001668 args.extend(['--verbose'] * verbose)
Edward Lemur227d5102020-02-25 23:45:35 +00001669
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001670 remote, remote_branch = self.GetRemoteBranch()
1671 target_ref = GetTargetRef(remote, remote_branch, None)
1672 if settings.GetIsGerrit():
1673 args.extend(['--gerrit_url', self.GetCodereviewServer()])
1674 args.extend(['--gerrit_project', self.GetGerritProject()])
1675 args.extend(['--gerrit_branch', target_ref])
Edward Lemur227d5102020-02-25 23:45:35 +00001676
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001677 author = self.GetAuthor()
1678 issue = self.GetIssue()
1679 patchset = self.GetPatchset()
1680 if author:
1681 args.extend(['--author', author])
1682 if issue:
1683 args.extend(['--issue', str(issue)])
1684 if patchset:
1685 args.extend(['--patchset', str(patchset)])
Edward Lesmeseb1bd622021-03-01 19:54:07 +00001686
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001687 return args
Edward Lemur227d5102020-02-25 23:45:35 +00001688
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001689 def RunHook(self,
1690 committing,
1691 may_prompt,
1692 verbose,
1693 parallel,
1694 upstream,
1695 description,
1696 all_files,
1697 files=None,
1698 resultdb=False,
1699 realm=None):
1700 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1701 args = self._GetCommonPresubmitArgs(verbose, upstream)
1702 args.append('--commit' if committing else '--upload')
1703 if may_prompt:
1704 args.append('--may_prompt')
1705 if parallel:
1706 args.append('--parallel')
1707 if all_files:
1708 args.append('--all_files')
1709 if files:
1710 args.extend(files.split(';'))
1711 args.append('--source_controlled_only')
1712 if files or all_files:
1713 args.append('--no_diffs')
Edward Lemur75526302020-02-27 22:31:05 +00001714
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001715 if resultdb and not realm:
1716 # TODO (crbug.com/1113463): store realm somewhere and look it up so
1717 # it is not required to pass the realm flag
1718 print(
1719 'Note: ResultDB reporting will NOT be performed because --realm'
1720 ' was not specified. To enable ResultDB, please run the command'
1721 ' again with the --realm argument to specify the LUCI realm.')
Edward Lemur227d5102020-02-25 23:45:35 +00001722
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001723 return self._RunPresubmit(args,
1724 description,
1725 resultdb=resultdb,
1726 realm=realm)
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001727
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001728 def _RunPresubmit(self,
1729 args: Sequence[str],
1730 description: str,
1731 resultdb: bool = False,
1732 realm: Optional[str] = None) -> Mapping[str, Any]:
1733 args = list(args)
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001734
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001735 with gclient_utils.temporary_file() as description_file:
1736 with gclient_utils.temporary_file() as json_output:
1737 gclient_utils.FileWrite(description_file, description)
1738 args.extend(['--json_output', json_output])
1739 args.extend(['--description_file', description_file])
1740 start = time_time()
1741 cmd = ['vpython3', PRESUBMIT_SUPPORT] + args
1742 if resultdb and realm:
1743 cmd = ['rdb', 'stream', '-new', '-realm', realm, '--'] + cmd
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001744
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001745 p = subprocess2.Popen(cmd)
1746 exit_code = p.wait()
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001747
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001748 metrics.collector.add_repeated(
1749 'sub_commands', {
1750 'command': 'presubmit',
1751 'execution_time': time_time() - start,
1752 'exit_code': exit_code,
1753 })
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001754
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001755 if exit_code:
1756 sys.exit(exit_code)
Edward Lemur227d5102020-02-25 23:45:35 +00001757
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001758 json_results = gclient_utils.FileRead(json_output)
1759 return json.loads(json_results)
Edward Lemur227d5102020-02-25 23:45:35 +00001760
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001761 def RunPostUploadHook(self, verbose, upstream, description):
1762 args = self._GetCommonPresubmitArgs(verbose, upstream)
1763 args.append('--post_upload')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001764
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001765 with gclient_utils.temporary_file() as description_file:
1766 gclient_utils.FileWrite(description_file, description)
1767 args.extend(['--description_file', description_file])
1768 subprocess2.Popen(['vpython3', PRESUBMIT_SUPPORT] + args).wait()
Edward Lemur75526302020-02-27 22:31:05 +00001769
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001770 def _GetDescriptionForUpload(self, options: optparse.Values,
1771 git_diff_args: Sequence[str],
1772 files: Sequence[str]) -> ChangeDescription:
1773 """Get description message for upload."""
1774 if self.GetIssue():
1775 description = self.FetchDescription()
1776 elif options.message:
1777 description = options.message
1778 else:
1779 description = _create_description_from_log(git_diff_args)
1780 if options.title and options.squash:
1781 description = options.title + '\n\n' + description
Edward Lemur75526302020-02-27 22:31:05 +00001782
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001783 bug = options.bug
1784 fixed = options.fixed
1785 if not self.GetIssue():
1786 # Extract bug number from branch name, but only if issue is being
1787 # created. It must start with bug or fix, followed by _ or - and
1788 # number. Optionally, it may contain _ or - after number with
1789 # arbitrary text. Examples: bug-123 bug_123 fix-123
1790 # fix-123-some-description
1791 branch = self.GetBranch()
1792 if branch is not None:
1793 match = re.match(
1794 r'^(?P<type>bug|fix(?:e[sd])?)[_-]?(?P<bugnum>\d+)([-_]|$)',
1795 branch)
1796 if not bug and not fixed and match:
1797 if match.group('type') == 'bug':
1798 bug = match.group('bugnum')
1799 else:
1800 fixed = match.group('bugnum')
Edward Lemur5a644f82020-03-18 16:44:57 +00001801
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001802 change_description = ChangeDescription(description, bug, fixed)
Edward Lemur5a644f82020-03-18 16:44:57 +00001803
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001804 # Fill gaps in OWNERS coverage to reviewers if requested.
1805 if options.add_owners_to:
1806 assert options.add_owners_to in ('R'), options.add_owners_to
1807 status = self.owners_client.GetFilesApprovalStatus(
1808 files, [], options.reviewers)
1809 missing_files = [
1810 f for f in files
1811 if status[f] == self._owners_client.INSUFFICIENT_REVIEWERS
1812 ]
1813 owners = self.owners_client.SuggestOwners(
1814 missing_files, exclude=[self.GetAuthor()])
1815 assert isinstance(options.reviewers, list), options.reviewers
1816 options.reviewers.extend(owners)
Edward Lemur5a644f82020-03-18 16:44:57 +00001817
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001818 # Set the reviewer list now so that presubmit checks can access it.
1819 if options.reviewers:
1820 change_description.update_reviewers(options.reviewers)
Edward Lesmes8c43c3f2021-01-20 00:20:26 +00001821
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001822 return change_description
Edward Lemur5a644f82020-03-18 16:44:57 +00001823
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001824 def _GetTitleForUpload(self, options, multi_change_upload=False):
1825 # type: (optparse.Values, Optional[bool]) -> str
Edward Lemur5a644f82020-03-18 16:44:57 +00001826
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001827 # Getting titles for multipl commits is not supported so we return the
1828 # default.
1829 if not options.squash or multi_change_upload or options.title:
1830 return options.title
Joanna Wanga1abbed2023-01-24 01:41:05 +00001831
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001832 # On first upload, patchset title is always this string, while
1833 # options.title gets converted to first line of message.
1834 if not self.GetIssue():
1835 return 'Initial upload'
Edward Lemur5a644f82020-03-18 16:44:57 +00001836
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001837 # When uploading subsequent patchsets, options.message is taken as the
1838 # title if options.title is not provided.
1839 if options.message:
1840 return options.message.strip()
Edward Lemur5a644f82020-03-18 16:44:57 +00001841
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001842 # Use the subject of the last commit as title by default.
1843 title = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
1844 if options.force or options.skip_title:
1845 return title
1846 user_title = gclient_utils.AskForData('Title for patchset [%s]: ' %
1847 title)
Edward Lemur5a644f82020-03-18 16:44:57 +00001848
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001849 # Use the default title if the user confirms the default with a 'y'.
1850 if user_title.lower() == 'y':
1851 return title
1852 return user_title or title
mlcui3da91712021-05-05 10:00:30 +00001853
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001854 def _GetRefSpecOptions(self,
1855 options: optparse.Values,
1856 change_desc: ChangeDescription,
1857 multi_change_upload: bool = False,
1858 dogfood_path: bool = False) -> List[str]:
1859 # Extra options that can be specified at push time. Doc:
1860 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
1861 refspec_opts = []
Edward Lemur5a644f82020-03-18 16:44:57 +00001862
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001863 # By default, new changes are started in WIP mode, and subsequent
1864 # patchsets don't send email. At any time, passing --send-mail or
1865 # --send-email will mark the change ready and send email for that
1866 # particular patch.
1867 if options.send_mail:
1868 refspec_opts.append('ready')
1869 refspec_opts.append('notify=ALL')
1870 elif (not self.GetIssue() and options.squash and not dogfood_path):
1871 refspec_opts.append('wip')
1872 else:
1873 refspec_opts.append('notify=NONE')
Joanna Wanga1abbed2023-01-24 01:41:05 +00001874
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001875 # TODO(tandrii): options.message should be posted as a comment if
1876 # --send-mail or --send-email is set on non-initial upload as Rietveld
1877 # used to do it.
Joanna Wanga1abbed2023-01-24 01:41:05 +00001878
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001879 # Set options.title in case user was prompted in _GetTitleForUpload and
1880 # _CMDUploadChange needs to be called again.
1881 options.title = self._GetTitleForUpload(
1882 options, multi_change_upload=multi_change_upload)
Joanna Wanga1abbed2023-01-24 01:41:05 +00001883
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001884 if options.title:
1885 # Punctuation and whitespace in |title| must be percent-encoded.
1886 refspec_opts.append(
1887 'm=' + gerrit_util.PercentEncodeForGitRef(options.title))
Joanna Wanga1abbed2023-01-24 01:41:05 +00001888
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001889 if options.private:
1890 refspec_opts.append('private')
Joanna Wanga1abbed2023-01-24 01:41:05 +00001891
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001892 if options.topic:
1893 # Documentation on Gerrit topics is here:
1894 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
1895 refspec_opts.append('topic=%s' % options.topic)
Joanna Wanga1abbed2023-01-24 01:41:05 +00001896
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001897 if options.enable_auto_submit:
1898 refspec_opts.append('l=Auto-Submit+1')
1899 if options.set_bot_commit:
1900 refspec_opts.append('l=Bot-Commit+1')
1901 if options.use_commit_queue:
1902 refspec_opts.append('l=Commit-Queue+2')
1903 elif options.cq_dry_run:
1904 refspec_opts.append('l=Commit-Queue+1')
Joanna Wanga1abbed2023-01-24 01:41:05 +00001905
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001906 if change_desc.get_reviewers(tbr_only=True):
1907 score = gerrit_util.GetCodeReviewTbrScore(self.GetGerritHost(),
1908 self.GetGerritProject())
1909 refspec_opts.append('l=Code-Review+%s' % score)
Joanna Wanga1abbed2023-01-24 01:41:05 +00001910
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001911 # Gerrit sorts hashtags, so order is not important.
1912 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
1913 # We check GetIssue because we only add hashtags from the
1914 # description on the first upload.
1915 # TODO(b/265929888): When we fully launch the new path:
1916 # 1) remove fetching hashtags from description alltogether
1917 # 2) Or use descrtiption hashtags for:
1918 # `not (self.GetIssue() and multi_change_upload)`
1919 # 3) Or enabled change description tags for multi and single changes
1920 # by adding them post `git push`.
1921 if not (self.GetIssue() and dogfood_path):
1922 hashtags.update(change_desc.get_hash_tags())
1923 refspec_opts.extend(['hashtag=%s' % t for t in hashtags])
Joanna Wanga1abbed2023-01-24 01:41:05 +00001924
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001925 # Note: Reviewers, and ccs are handled individually for each
1926 # branch/change.
1927 return refspec_opts
Joanna Wang40497912023-01-24 21:18:16 +00001928
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001929 def PrepareSquashedCommit(self,
1930 options: optparse.Values,
1931 parent: str,
1932 orig_parent: str,
1933 end_commit: Optional[str] = None) -> _NewUpload:
1934 """Create a squashed commit to upload.
Joanna Wang05b60342023-03-29 20:25:57 +00001935
1936
1937 Args:
1938 parent: The commit to use as the parent for the new squashed.
1939 orig_parent: The commit that is an actual ancestor of `end_commit`. It
1940 is part of the same original tree as end_commit, which does not
1941 contain squashed commits. This is used to create the change
1942 description for the new squashed commit with:
1943 `git log orig_parent..end_commit`.
1944 end_commit: The commit to use as the end of the new squashed commit.
1945 """
Joanna Wangb88a4342023-01-24 01:28:22 +00001946
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001947 if end_commit is None:
1948 end_commit = RunGit(['rev-parse', self.branchref]).strip()
Joanna Wangb88a4342023-01-24 01:28:22 +00001949
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001950 reviewers, ccs, change_desc = self._PrepareChange(
1951 options, orig_parent, end_commit)
1952 latest_tree = RunGit(['rev-parse', end_commit + ':']).strip()
1953 with gclient_utils.temporary_file() as desc_tempfile:
1954 gclient_utils.FileWrite(desc_tempfile, change_desc.description)
1955 commit_to_push = RunGit(
1956 ['commit-tree', latest_tree, '-p', parent, '-F',
1957 desc_tempfile]).strip()
Joanna Wangb88a4342023-01-24 01:28:22 +00001958
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001959 # Gerrit may or may not update fast enough to return the correct
1960 # patchset number after we push. Get the pre-upload patchset and
1961 # increment later.
1962 prev_patchset = self.GetMostRecentPatchset(update=False) or 0
1963 return _NewUpload(reviewers, ccs, commit_to_push, end_commit, parent,
1964 change_desc, prev_patchset)
Joanna Wangb88a4342023-01-24 01:28:22 +00001965
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001966 def PrepareCherryPickSquashedCommit(self, options: optparse.Values,
1967 parent: str) -> _NewUpload:
1968 """Create a commit cherry-picked on parent to push."""
Joanna Wange8523912023-01-21 02:05:40 +00001969
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001970 # The `parent` is what we will cherry-pick on top of.
1971 # The `cherry_pick_base` is the beginning range of what
1972 # we are cherry-picking.
1973 cherry_pick_base = self.GetCommonAncestorWithUpstream()
1974 reviewers, ccs, change_desc = self._PrepareChange(
1975 options, cherry_pick_base, self.branchref)
Joanna Wange8523912023-01-21 02:05:40 +00001976
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001977 new_upload_hash = RunGit(['rev-parse', self.branchref]).strip()
1978 latest_tree = RunGit(['rev-parse', self.branchref + ':']).strip()
1979 with gclient_utils.temporary_file() as desc_tempfile:
1980 gclient_utils.FileWrite(desc_tempfile, change_desc.description)
1981 commit_to_cp = RunGit([
1982 'commit-tree', latest_tree, '-p', cherry_pick_base, '-F',
1983 desc_tempfile
1984 ]).strip()
Joanna Wange8523912023-01-21 02:05:40 +00001985
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001986 RunGit(['checkout', '-q', parent])
1987 ret, _out = RunGitWithCode(['cherry-pick', commit_to_cp])
1988 if ret:
1989 RunGit(['cherry-pick', '--abort'])
1990 RunGit(['checkout', '-q', self.branch])
1991 DieWithError('Could not cleanly cherry-pick')
Joanna Wange8523912023-01-21 02:05:40 +00001992
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001993 commit_to_push = RunGit(['rev-parse', 'HEAD']).strip()
1994 RunGit(['checkout', '-q', self.branch])
Joanna Wange8523912023-01-21 02:05:40 +00001995
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001996 # Gerrit may or may not update fast enough to return the correct
1997 # patchset number after we push. Get the pre-upload patchset and
1998 # increment later.
1999 prev_patchset = self.GetMostRecentPatchset(update=False) or 0
2000 return _NewUpload(reviewers, ccs, commit_to_push, new_upload_hash,
2001 cherry_pick_base, change_desc, prev_patchset)
Joanna Wange8523912023-01-21 02:05:40 +00002002
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002003 def _PrepareChange(
2004 self, options: optparse.Values, parent: str, end_commit: str
2005 ) -> Tuple[Sequence[str], Sequence[str], ChangeDescription]:
2006 """Prepares the change to be uploaded."""
2007 self.EnsureCanUploadPatchset(options.force)
Joanna Wangb46232e2023-01-21 01:58:46 +00002008
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002009 files = self.GetAffectedFiles(parent, end_commit=end_commit)
2010 change_desc = self._GetDescriptionForUpload(options,
2011 [parent, end_commit], files)
Joanna Wangb46232e2023-01-21 01:58:46 +00002012
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002013 watchlist = watchlists.Watchlists(settings.GetRoot())
2014 self.ExtendCC(watchlist.GetWatchersForPaths(files))
2015 if not options.bypass_hooks:
2016 hook_results = self.RunHook(committing=False,
2017 may_prompt=not options.force,
2018 verbose=options.verbose,
2019 parallel=options.parallel,
2020 upstream=parent,
2021 description=change_desc.description,
2022 all_files=False)
2023 self.ExtendCC(hook_results['more_cc'])
Joanna Wangb46232e2023-01-21 01:58:46 +00002024
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002025 # Update the change description and ensure we have a Change Id.
2026 if self.GetIssue():
2027 if options.edit_description:
2028 change_desc.prompt()
2029 change_detail = self._GetChangeDetail(['CURRENT_REVISION'])
2030 change_id = change_detail['change_id']
2031 change_desc.ensure_change_id(change_id)
Joanna Wangb46232e2023-01-21 01:58:46 +00002032
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002033 else: # No change issue. First time uploading
2034 if not options.force and not options.message_file:
2035 change_desc.prompt()
Joanna Wangb46232e2023-01-21 01:58:46 +00002036
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002037 # Check if user added a change_id in the descripiton.
2038 change_ids = git_footers.get_footer_change_id(
2039 change_desc.description)
2040 if len(change_ids) == 1:
2041 change_id = change_ids[0]
2042 else:
2043 change_id = GenerateGerritChangeId(change_desc.description)
2044 change_desc.ensure_change_id(change_id)
Joanna Wangb46232e2023-01-21 01:58:46 +00002045
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002046 if options.preserve_tryjobs:
2047 change_desc.set_preserve_tryjobs()
Joanna Wangb46232e2023-01-21 01:58:46 +00002048
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002049 SaveDescriptionBackup(change_desc)
Joanna Wangb46232e2023-01-21 01:58:46 +00002050
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002051 # Add ccs
2052 ccs = []
2053 # Add default, watchlist, presubmit ccs if this is the initial upload
2054 # and CL is not private and auto-ccing has not been disabled.
2055 if not options.private and not options.no_autocc and not self.GetIssue(
2056 ):
2057 ccs = self.GetCCList().split(',')
2058 if len(ccs) > 100:
2059 lsc = (
2060 'https://chromium.googlesource.com/chromium/src/+/HEAD/docs/'
2061 'process/lsc/lsc_workflow.md')
2062 print('WARNING: This will auto-CC %s users.' % len(ccs))
2063 print('LSC may be more appropriate: %s' % lsc)
2064 print(
2065 'You can also use the --no-autocc flag to disable auto-CC.')
2066 confirm_or_exit(action='continue')
Joanna Wangb46232e2023-01-21 01:58:46 +00002067
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002068 # Add ccs from the --cc flag.
2069 if options.cc:
2070 ccs.extend(options.cc)
Joanna Wangb46232e2023-01-21 01:58:46 +00002071
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002072 ccs = [email.strip() for email in ccs if email.strip()]
2073 if change_desc.get_cced():
2074 ccs.extend(change_desc.get_cced())
Joanna Wangb46232e2023-01-21 01:58:46 +00002075
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002076 return change_desc.get_reviewers(), ccs, change_desc
Joanna Wangb46232e2023-01-21 01:58:46 +00002077
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002078 def PostUploadUpdates(self, options: optparse.Values,
2079 new_upload: _NewUpload, change_number: str) -> None:
2080 """Makes necessary post upload changes to the local and remote cl."""
2081 if not self.GetIssue():
2082 self.SetIssue(change_number)
Joanna Wang40497912023-01-24 21:18:16 +00002083
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002084 self.SetPatchset(new_upload.prev_patchset + 1)
Joanna Wang7603f042023-03-01 22:17:36 +00002085
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002086 self._GitSetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY,
2087 new_upload.commit_to_push)
2088 self._GitSetBranchConfigValue(LAST_UPLOAD_HASH_CONFIG_KEY,
2089 new_upload.new_last_uploaded_commit)
Joanna Wang40497912023-01-24 21:18:16 +00002090
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002091 if settings.GetRunPostUploadHook():
2092 self.RunPostUploadHook(options.verbose, new_upload.parent,
2093 new_upload.change_desc.description)
Joanna Wang40497912023-01-24 21:18:16 +00002094
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002095 if new_upload.reviewers or new_upload.ccs:
2096 gerrit_util.AddReviewers(self.GetGerritHost(),
2097 self._GerritChangeIdentifier(),
2098 reviewers=new_upload.reviewers,
2099 ccs=new_upload.ccs,
2100 notify=bool(options.send_mail))
Joanna Wang40497912023-01-24 21:18:16 +00002101
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002102 def CMDUpload(self, options, git_diff_args, orig_args):
2103 """Uploads a change to codereview."""
2104 custom_cl_base = None
2105 if git_diff_args:
2106 custom_cl_base = base_branch = git_diff_args[0]
2107 else:
2108 if self.GetBranch() is None:
2109 DieWithError(
2110 'Can\'t upload from detached HEAD state. Get on a branch!')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002111
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002112 # Default to diffing against common ancestor of upstream branch
2113 base_branch = self.GetCommonAncestorWithUpstream()
2114 git_diff_args = [base_branch, 'HEAD']
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002115
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002116 # Fast best-effort checks to abort before running potentially expensive
2117 # hooks if uploading is likely to fail anyway. Passing these checks does
2118 # not guarantee that uploading will not fail.
2119 self.EnsureAuthenticated(force=options.force)
2120 self.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002121
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002122 print(f'Processing {_GetCommitCountSummary(*git_diff_args)}...')
Daniel Cheng66d0f152023-08-29 23:21:58 +00002123
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002124 # Apply watchlists on upload.
2125 watchlist = watchlists.Watchlists(settings.GetRoot())
2126 files = self.GetAffectedFiles(base_branch)
2127 if not options.bypass_watchlists:
2128 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002129
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002130 change_desc = self._GetDescriptionForUpload(options, git_diff_args,
2131 files)
2132 if not options.bypass_hooks:
2133 hook_results = self.RunHook(committing=False,
2134 may_prompt=not options.force,
2135 verbose=options.verbose,
2136 parallel=options.parallel,
2137 upstream=base_branch,
2138 description=change_desc.description,
2139 all_files=False)
2140 self.ExtendCC(hook_results['more_cc'])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002141
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002142 print_stats(git_diff_args)
2143 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base,
2144 change_desc)
2145 if not ret:
2146 if self.GetBranch() is not None:
2147 self._GitSetBranchConfigValue(
2148 LAST_UPLOAD_HASH_CONFIG_KEY,
2149 scm.GIT.ResolveCommit(settings.GetRoot(), 'HEAD'))
2150 # Run post upload hooks, if specified.
2151 if settings.GetRunPostUploadHook():
2152 self.RunPostUploadHook(options.verbose, base_branch,
2153 change_desc.description)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002154
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002155 # Upload all dependencies if specified.
2156 if options.dependencies:
2157 print()
2158 print('--dependencies has been specified.')
2159 print('All dependent local branches will be re-uploaded.')
2160 print()
2161 # Remove the dependencies flag from args so that we do not end
2162 # up in a loop.
2163 orig_args.remove('--dependencies')
2164 ret = upload_branch_deps(self, orig_args, options.force)
2165 return ret
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002166
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002167 def SetCQState(self, new_state):
2168 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002169
Struan Shrimpton8b2072b2023-07-31 21:01:26 +00002170 Issue must have been already uploaded and known.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002171 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002172 assert new_state in _CQState.ALL_STATES
2173 assert self.GetIssue()
2174 try:
2175 vote_map = {
2176 _CQState.NONE: 0,
2177 _CQState.DRY_RUN: 1,
2178 _CQState.COMMIT: 2,
2179 }
2180 labels = {'Commit-Queue': vote_map[new_state]}
2181 notify = False if new_state == _CQState.DRY_RUN else None
2182 gerrit_util.SetReview(self.GetGerritHost(),
2183 self._GerritChangeIdentifier(),
2184 labels=labels,
2185 notify=notify)
2186 return 0
2187 except KeyboardInterrupt:
2188 raise
2189 except:
2190 print(
2191 'WARNING: Failed to %s.\n'
2192 'Either:\n'
2193 ' * Your project has no CQ,\n'
2194 ' * You don\'t have permission to change the CQ state,\n'
2195 ' * There\'s a bug in this code (see stack trace below).\n'
2196 'Consider specifying which bots to trigger manually or asking your '
2197 'project owners for permissions or contacting Chrome Infra at:\n'
2198 'https://www.chromium.org/infra\n\n' %
2199 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
2200 # Still raise exception so that stack trace is printed.
2201 raise
qyearsley1fdfcb62016-10-24 13:22:03 -07002202
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002203 def GetGerritHost(self):
2204 # Lazy load of configs.
2205 self.GetCodereviewServer()
Aravind Vasudevan0e3589e2023-05-25 23:00:30 +00002206
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002207 if self._gerrit_host and '.' not in self._gerrit_host:
2208 # Abbreviated domain like "chromium" instead of
2209 # chromium.googlesource.com.
2210 parsed = urllib.parse.urlparse(self.GetRemoteUrl())
2211 if parsed.scheme == 'sso':
2212 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2213 self._gerrit_server = 'https://%s' % self._gerrit_host
Aravind Vasudevan0e3589e2023-05-25 23:00:30 +00002214
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002215 return self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002216
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002217 def _GetGitHost(self):
2218 """Returns git host to be used when uploading change to Gerrit."""
2219 remote_url = self.GetRemoteUrl()
2220 if not remote_url:
2221 return None
2222 return urllib.parse.urlparse(remote_url).netloc
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002223
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002224 def GetCodereviewServer(self):
2225 if not self._gerrit_server:
2226 # If we're on a branch then get the server potentially associated
2227 # with that branch.
2228 if self.GetIssue() and self.GetBranch():
2229 self._gerrit_server = self._GitGetBranchConfigValue(
2230 CODEREVIEW_SERVER_CONFIG_KEY)
2231 if self._gerrit_server:
2232 self._gerrit_host = urllib.parse.urlparse(
2233 self._gerrit_server).netloc
2234 if not self._gerrit_server:
2235 url = urllib.parse.urlparse(self.GetRemoteUrl())
2236 parts = url.netloc.split('.')
Aravind Vasudevan0e3589e2023-05-25 23:00:30 +00002237
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002238 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2239 # has "-review" suffix for lowest level subdomain.
2240 parts[0] = parts[0] + '-review'
Aravind Vasudevan0e3589e2023-05-25 23:00:30 +00002241
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002242 if url.scheme == 'sso' and len(parts) == 1:
2243 # sso:// uses abbreivated hosts, eg. sso://chromium instead
2244 # of chromium.googlesource.com. Hence, for code review
2245 # server, they need to be expanded.
2246 parts[0] += '.googlesource.com'
Aravind Vasudevan0e3589e2023-05-25 23:00:30 +00002247
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002248 self._gerrit_host = '.'.join(parts)
2249 self._gerrit_server = 'https://%s' % self._gerrit_host
2250 return self._gerrit_server
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002251
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002252 def GetGerritProject(self):
2253 """Returns Gerrit project name based on remote git URL."""
2254 remote_url = self.GetRemoteUrl()
2255 if remote_url is None:
2256 logging.warning('can\'t detect Gerrit project.')
2257 return None
2258 project = urllib.parse.urlparse(remote_url).path.strip('/')
2259 if project.endswith('.git'):
2260 project = project[:-len('.git')]
2261 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start
2262 # with 'a/' prefix, because 'a/' prefix is used to force authentication
2263 # in gitiles/git-over-https protocol. E.g.,
2264 # https://chromium.googlesource.com/a/v8/v8 refers to the same
2265 # repo/project as https://chromium.googlesource.com/v8/v8
2266 if project.startswith('a/'):
2267 project = project[len('a/'):]
2268 return project
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002269
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002270 def _GerritChangeIdentifier(self):
2271 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002272
2273 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002274 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002275 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002276 project = self.GetGerritProject()
2277 if project:
2278 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
2279 # Fall back on still unique, but less efficient change number.
2280 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002281
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002282 def EnsureAuthenticated(self, force, refresh=None):
2283 """Best effort check that user is authenticated with Gerrit server."""
2284 if settings.GetGerritSkipEnsureAuthenticated():
2285 # For projects with unusual authentication schemes.
2286 # See http://crbug.com/603378.
2287 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002288
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002289 # Check presence of cookies only if using cookies-based auth method.
2290 cookie_auth = gerrit_util.Authenticator.get()
2291 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
2292 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002293
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002294 remote_url = self.GetRemoteUrl()
2295 if remote_url is None:
2296 logging.warning('invalid remote')
2297 return
2298 if urllib.parse.urlparse(remote_url).scheme not in ['https', 'sso']:
2299 logging.warning(
2300 'Ignoring branch %(branch)s with non-https/sso remote '
2301 '%(remote)s', {
2302 'branch': self.branch,
2303 'remote': self.GetRemoteUrl()
2304 })
2305 return
Daniel Chengcf6269b2019-05-18 01:02:12 +00002306
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002307 # Lazy-loader to identify Gerrit and Git hosts.
2308 self.GetCodereviewServer()
2309 git_host = self._GetGitHost()
2310 assert self._gerrit_server and self._gerrit_host and git_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002311
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002312 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2313 git_auth = cookie_auth.get_auth_header(git_host)
2314 if gerrit_auth and git_auth:
2315 if gerrit_auth == git_auth:
2316 return
2317 all_gsrc = cookie_auth.get_auth_header(
2318 'd0esN0tEx1st.googlesource.com')
2319 print(
2320 'WARNING: You have different credentials for Gerrit and git hosts:\n'
2321 ' %s\n'
2322 ' %s\n'
2323 ' Consider running the following command:\n'
2324 ' git cl creds-check\n'
2325 ' %s\n'
2326 ' %s' %
2327 (git_host, self._gerrit_host,
2328 ('Hint: delete creds for .googlesource.com' if all_gsrc else
2329 ''), cookie_auth.get_new_password_message(git_host)))
2330 if not force:
2331 confirm_or_exit('If you know what you are doing',
2332 action='continue')
2333 return
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00002334
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002335 missing = (([] if gerrit_auth else [self._gerrit_host]) +
2336 ([] if git_auth else [git_host]))
2337 DieWithError('Credentials for the following hosts are required:\n'
2338 ' %s\n'
2339 'These are read from %s (or legacy %s)\n'
2340 '%s' %
2341 ('\n '.join(missing), cookie_auth.get_gitcookies_path(),
2342 cookie_auth.get_netrc_path(),
2343 cookie_auth.get_new_password_message(git_host)))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002344
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002345 def EnsureCanUploadPatchset(self, force):
2346 if not self.GetIssue():
2347 return
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002348
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002349 status = self._GetChangeDetail()['status']
2350 if status == 'ABANDONED':
2351 DieWithError(
2352 'Change %s has been abandoned, new uploads are not allowed' %
2353 (self.GetIssueURL()))
2354 if status == 'MERGED':
2355 answer = gclient_utils.AskForData(
2356 'Change %s has been submitted, new uploads are not allowed. '
2357 'Would you like to start a new change (Y/n)?' %
2358 self.GetIssueURL()).lower()
2359 if answer not in ('y', ''):
2360 DieWithError('New uploads are not allowed.')
2361 self.SetIssue()
2362 return
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002363
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002364 # TODO(vadimsh): For some reason the chunk of code below was skipped if
2365 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
2366 # Apparently this check is not very important? Otherwise get_auth_email
2367 # could have been added to other implementations of Authenticator.
2368 cookies_auth = gerrit_util.Authenticator.get()
2369 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
2370 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002371
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002372 cookies_user = cookies_auth.get_auth_email(self.GetGerritHost())
2373 if self.GetIssueOwner() == cookies_user:
2374 return
2375 logging.debug('change %s owner is %s, cookies user is %s',
2376 self.GetIssue(), self.GetIssueOwner(), cookies_user)
2377 # Maybe user has linked accounts or something like that,
2378 # so ask what Gerrit thinks of this user.
2379 details = gerrit_util.GetAccountDetails(self.GetGerritHost(), 'self')
2380 if details['email'] == self.GetIssueOwner():
2381 return
2382 if not force:
2383 print(
2384 'WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
2385 'as %s.\n'
2386 'Uploading may fail due to lack of permissions.' %
2387 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2388 confirm_or_exit(action='upload')
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002389
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002390 def GetStatus(self):
2391 """Applies a rough heuristic to give a simple summary of an issue's review
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002392 or CQ status, assuming adherence to a common workflow.
2393
2394 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002395 * 'error' - error from review tool (including deleted issues)
2396 * 'unsent' - no reviewers added
2397 * 'waiting' - waiting for review
2398 * 'reply' - waiting for uploader to reply to review
2399 * 'lgtm' - Code-Review label has been set
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00002400 * 'dry-run' - dry-running in the CQ
2401 * 'commit' - in the CQ
Aaron Gable9ab38c62017-04-06 14:36:33 -07002402 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002403 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002404 if not self.GetIssue():
2405 return None
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002406
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002407 try:
2408 data = self._GetChangeDetail(
2409 ['DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
2410 except GerritChangeNotExists:
2411 return 'error'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002412
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002413 if data['status'] in ('ABANDONED', 'MERGED'):
2414 return 'closed'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002415
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002416 cq_label = data['labels'].get('Commit-Queue', {})
2417 max_cq_vote = 0
2418 for vote in cq_label.get('all', []):
2419 max_cq_vote = max(max_cq_vote, vote.get('value', 0))
2420 if max_cq_vote == 2:
2421 return 'commit'
2422 if max_cq_vote == 1:
2423 return 'dry-run'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002424
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002425 if data['labels'].get('Code-Review', {}).get('approved'):
2426 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002427
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002428 if not data.get('reviewers', {}).get('REVIEWER', []):
2429 return 'unsent'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002430
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002431 owner = data['owner'].get('_account_id')
2432 messages = sorted(data.get('messages', []), key=lambda m: m.get('date'))
2433 while messages:
2434 m = messages.pop()
2435 if (m.get('tag', '').startswith('autogenerated:cq')
2436 or m.get('tag', '').startswith('autogenerated:cv')):
2437 # Ignore replies from LUCI CV/CQ.
2438 continue
2439 if m.get('author', {}).get('_account_id') == owner:
2440 # Most recent message was by owner.
2441 return 'waiting'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00002442
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002443 # Some reply from non-owner.
2444 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002445
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002446 # Somehow there are no messages even though there are reviewers.
2447 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002448
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002449 def GetMostRecentPatchset(self, update=True):
2450 if not self.GetIssue():
2451 return None
Edward Lemur6c6827c2020-02-06 21:15:18 +00002452
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002453 data = self._GetChangeDetail(['CURRENT_REVISION'])
2454 patchset = data['revisions'][data['current_revision']]['_number']
2455 if update:
2456 self.SetPatchset(patchset)
2457 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002458
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002459 def _IsPatchsetRangeSignificant(self, lower, upper):
2460 """Returns True if the inclusive range of patchsets contains any reworks or
Gavin Makf35a9eb2022-11-17 18:34:36 +00002461 rebases."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002462 if not self.GetIssue():
2463 return False
Gavin Makf35a9eb2022-11-17 18:34:36 +00002464
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002465 data = self._GetChangeDetail(['ALL_REVISIONS'])
2466 ps_kind = {}
2467 for rev_info in data.get('revisions', {}).values():
2468 ps_kind[rev_info['_number']] = rev_info.get('kind', '')
Gavin Makf35a9eb2022-11-17 18:34:36 +00002469
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002470 for ps in range(lower, upper + 1):
2471 assert ps in ps_kind, 'expected patchset %d in change detail' % ps
2472 if ps_kind[ps] not in ('NO_CHANGE', 'NO_CODE_CHANGE'):
2473 return True
2474 return False
Gavin Makf35a9eb2022-11-17 18:34:36 +00002475
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002476 def GetMostRecentDryRunPatchset(self):
2477 """Get patchsets equivalent to the most recent patchset and return
Gavin Make61ccc52020-11-13 00:12:57 +00002478 the patchset with the latest dry run. If none have been dry run, return
2479 the latest patchset."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002480 if not self.GetIssue():
2481 return None
Gavin Make61ccc52020-11-13 00:12:57 +00002482
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002483 data = self._GetChangeDetail(['ALL_REVISIONS'])
2484 patchset = data['revisions'][data['current_revision']]['_number']
2485 dry_run = {
2486 int(m['_revision_number'])
2487 for m in data.get('messages', [])
2488 if m.get('tag', '').endswith('dry-run')
2489 }
Gavin Make61ccc52020-11-13 00:12:57 +00002490
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002491 for revision_info in sorted(data.get('revisions', {}).values(),
2492 key=lambda c: c['_number'],
2493 reverse=True):
2494 if revision_info['_number'] in dry_run:
2495 patchset = revision_info['_number']
2496 break
2497 if revision_info.get('kind', '') not in \
2498 ('NO_CHANGE', 'NO_CODE_CHANGE', 'TRIVIAL_REBASE'):
2499 break
2500 self.SetPatchset(patchset)
2501 return patchset
Gavin Make61ccc52020-11-13 00:12:57 +00002502
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002503 def AddComment(self, message, publish=None):
2504 gerrit_util.SetReview(self.GetGerritHost(),
2505 self._GerritChangeIdentifier(),
2506 msg=message,
2507 ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002508
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002509 def GetCommentsSummary(self, readable=True):
2510 # DETAILED_ACCOUNTS is to get emails in accounts.
2511 # CURRENT_REVISION is included to get the latest patchset so that
2512 # only the robot comments from the latest patchset can be shown.
2513 messages = self._GetChangeDetail(
2514 options=['MESSAGES', 'DETAILED_ACCOUNTS', 'CURRENT_REVISION']).get(
2515 'messages', [])
2516 file_comments = gerrit_util.GetChangeComments(
2517 self.GetGerritHost(), self._GerritChangeIdentifier())
2518 robot_file_comments = gerrit_util.GetChangeRobotComments(
2519 self.GetGerritHost(), self._GerritChangeIdentifier())
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002520
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002521 # Add the robot comments onto the list of comments, but only
2522 # keep those that are from the latest patchset.
2523 latest_patch_set = self.GetMostRecentPatchset()
2524 for path, robot_comments in robot_file_comments.items():
2525 line_comments = file_comments.setdefault(path, [])
2526 line_comments.extend([
2527 c for c in robot_comments if c['patch_set'] == latest_patch_set
2528 ])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002529
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002530 # Build dictionary of file comments for easy access and sorting later.
2531 # {author+date: {path: {patchset: {line: url+message}}}}
2532 comments = collections.defaultdict(lambda: collections.defaultdict(
2533 lambda: collections.defaultdict(dict)))
Andrii Shyshkalova3762a92020-11-25 10:20:42 +00002534
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002535 server = self.GetCodereviewServer()
2536 if server in _KNOWN_GERRIT_TO_SHORT_URLS:
2537 # /c/ is automatically added by short URL server.
2538 url_prefix = '%s/%s' % (_KNOWN_GERRIT_TO_SHORT_URLS[server],
2539 self.GetIssue())
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002540 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002541 url_prefix = '%s/c/%s' % (server, self.GetIssue())
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002542
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002543 for path, line_comments in file_comments.items():
2544 for comment in line_comments:
2545 tag = comment.get('tag', '')
2546 if tag.startswith(
2547 'autogenerated') and 'robot_id' not in comment:
2548 continue
2549 key = (comment['author']['email'], comment['updated'])
2550 if comment.get('side', 'REVISION') == 'PARENT':
2551 patchset = 'Base'
2552 else:
2553 patchset = 'PS%d' % comment['patch_set']
2554 line = comment.get('line', 0)
2555 url = ('%s/%s/%s#%s%s' %
2556 (url_prefix, comment['patch_set'],
2557 path, 'b' if comment.get('side') == 'PARENT' else '',
2558 str(line) if line else ''))
2559 comments[key][path][patchset][line] = (url, comment['message'])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002560
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002561 summaries = []
2562 for msg in messages:
2563 summary = self._BuildCommentSummary(msg, comments, readable)
2564 if summary:
2565 summaries.append(summary)
2566 return summaries
Josip Sokcevic266129c2021-11-09 00:22:00 +00002567
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002568 @staticmethod
2569 def _BuildCommentSummary(msg, comments, readable):
2570 if 'email' not in msg['author']:
2571 # Some bot accounts may not have an email associated.
2572 return None
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002573
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002574 key = (msg['author']['email'], msg['date'])
2575 # Don't bother showing autogenerated messages that don't have associated
2576 # file or line comments. this will filter out most autogenerated
2577 # messages, but will keep robot comments like those from Tricium.
2578 is_autogenerated = msg.get('tag', '').startswith('autogenerated')
2579 if is_autogenerated and not comments.get(key):
2580 return None
2581 message = msg['message']
2582 # Gerrit spits out nanoseconds.
2583 assert len(msg['date'].split('.')[-1]) == 9
2584 date = datetime.datetime.strptime(msg['date'][:-3],
2585 '%Y-%m-%d %H:%M:%S.%f')
2586 if key in comments:
2587 message += '\n'
2588 for path, patchsets in sorted(comments.get(key, {}).items()):
2589 if readable:
2590 message += '\n%s' % path
2591 for patchset, lines in sorted(patchsets.items()):
2592 for line, (url, content) in sorted(lines.items()):
2593 if line:
2594 line_str = 'Line %d' % line
2595 path_str = '%s:%d:' % (path, line)
2596 else:
2597 line_str = 'File comment'
2598 path_str = '%s:0:' % path
2599 if readable:
2600 message += '\n %s, %s: %s' % (patchset, line_str, url)
2601 message += '\n %s\n' % content
2602 else:
2603 message += '\n%s ' % path_str
2604 message += '\n%s\n' % content
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002605
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002606 return _CommentSummary(
2607 date=date,
2608 message=message,
2609 sender=msg['author']['email'],
2610 autogenerated=is_autogenerated,
2611 # These could be inferred from the text messages and correlated with
2612 # Code-Review label maximum, however this is not reliable.
2613 # Leaving as is until the need arises.
2614 approval=False,
2615 disapproval=False,
2616 )
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002617
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002618 def CloseIssue(self):
2619 gerrit_util.AbandonChange(self.GetGerritHost(),
2620 self._GerritChangeIdentifier(),
2621 msg='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002622
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002623 def SubmitIssue(self):
2624 gerrit_util.SubmitChange(self.GetGerritHost(),
2625 self._GerritChangeIdentifier())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002626
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002627 def _GetChangeDetail(self, options=None):
2628 """Returns details of associated Gerrit change and caching results."""
2629 options = options or []
2630 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002631
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002632 # Optimization to avoid multiple RPCs:
2633 if 'CURRENT_REVISION' in options or 'ALL_REVISIONS' in options:
2634 options.append('CURRENT_COMMIT')
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002635
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002636 # Normalize issue and options for consistent keys in cache.
2637 cache_key = str(self.GetIssue())
2638 options_set = frozenset(o.upper() for o in options)
2639
2640 for cached_options_set, data in self._detail_cache.get(cache_key, []):
2641 # Assumption: data fetched before with extra options is suitable
2642 # for return for a smaller set of options.
2643 # For example, if we cached data for
2644 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2645 # and request is for options=[CURRENT_REVISION],
2646 # THEN we can return prior cached data.
2647 if options_set.issubset(cached_options_set):
2648 return data
2649
2650 try:
2651 data = gerrit_util.GetChangeDetail(self.GetGerritHost(),
2652 self._GerritChangeIdentifier(),
2653 options_set)
2654 except gerrit_util.GerritError as e:
2655 if e.http_status == 404:
2656 raise GerritChangeNotExists(self.GetIssue(),
2657 self.GetCodereviewServer())
2658 raise
2659
2660 self._detail_cache.setdefault(cache_key, []).append((options_set, data))
Edward Lesmes7677e5c2020-02-19 20:39:03 +00002661 return data
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002662
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002663 def _GetChangeCommit(self, revision='current'):
2664 assert self.GetIssue(), 'issue must be set to query Gerrit'
2665 try:
2666 data = gerrit_util.GetChangeCommit(self.GetGerritHost(),
2667 self._GerritChangeIdentifier(),
2668 revision)
2669 except gerrit_util.GerritError as e:
2670 if e.http_status == 404:
2671 raise GerritChangeNotExists(self.GetIssue(),
2672 self.GetCodereviewServer())
2673 raise
2674 return data
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002675
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002676 def _IsCqConfigured(self):
2677 detail = self._GetChangeDetail(['LABELS'])
2678 return u'Commit-Queue' in detail.get('labels', {})
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002679
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002680 def CMDLand(self, force, bypass_hooks, verbose, parallel, resultdb, realm):
2681 if git_common.is_dirty_git_tree('land'):
2682 return 1
agable32978d92016-11-01 12:55:02 -07002683
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002684 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2685 if not force and self._IsCqConfigured():
2686 confirm_or_exit(
2687 '\nIt seems this repository has a CQ, '
2688 'which can test and land changes for you. '
2689 'Are you sure you wish to bypass it?\n',
2690 action='bypass CQ')
2691 differs = True
2692 last_upload = self._GitGetBranchConfigValue(
Gavin Mak4e5e3992022-11-14 22:40:12 +00002693 GERRIT_SQUASH_HASH_CONFIG_KEY)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002694 # Note: git diff outputs nothing if there is no diff.
2695 if not last_upload or RunGit(['diff', last_upload]).strip():
2696 print(
2697 'WARNING: Some changes from local branch haven\'t been uploaded.'
2698 )
Edward Lemur5a644f82020-03-18 16:44:57 +00002699 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002700 if detail['current_revision'] == last_upload:
2701 differs = False
2702 else:
2703 print(
2704 'WARNING: Local branch contents differ from latest uploaded '
2705 'patchset.')
2706 if differs:
2707 if not force:
2708 confirm_or_exit(
2709 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2710 action='submit')
2711 print(
2712 'WARNING: Bypassing hooks and submitting latest uploaded patchset.'
2713 )
2714 elif not bypass_hooks:
2715 upstream = self.GetCommonAncestorWithUpstream()
2716 if self.GetIssue():
2717 description = self.FetchDescription()
2718 else:
2719 description = _create_description_from_log([upstream])
2720 self.RunHook(committing=True,
2721 may_prompt=not force,
2722 verbose=verbose,
2723 parallel=parallel,
2724 upstream=upstream,
2725 description=description,
2726 all_files=False,
2727 resultdb=resultdb,
2728 realm=realm)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002729
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002730 self.SubmitIssue()
2731 print('Issue %s has been submitted.' % self.GetIssueURL())
2732 links = self._GetChangeCommit().get('web_links', [])
2733 for link in links:
2734 if link.get('name') in ['gitiles', 'browse'] and link.get('url'):
2735 print('Landed as: %s' % link.get('url'))
2736 break
2737 return 0
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002738
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002739 def CMDPatchWithParsedIssue(self, parsed_issue_arg, nocommit, force,
2740 newbranch):
2741 assert parsed_issue_arg.valid
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002742
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002743 self.issue = parsed_issue_arg.issue
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002744
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002745 if parsed_issue_arg.hostname:
2746 self._gerrit_host = parsed_issue_arg.hostname
2747 self._gerrit_server = 'https://%s' % self._gerrit_host
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002748
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002749 try:
2750 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2751 except GerritChangeNotExists as e:
2752 DieWithError(str(e))
agablec6787972016-09-09 16:13:34 -07002753
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002754 if not parsed_issue_arg.patchset:
2755 # Use current revision by default.
2756 revision_info = detail['revisions'][detail['current_revision']]
2757 patchset = int(revision_info['_number'])
2758 else:
2759 patchset = parsed_issue_arg.patchset
2760 for revision_info in detail['revisions'].values():
2761 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2762 break
2763 else:
2764 DieWithError('Couldn\'t find patchset %i in change %i' %
2765 (parsed_issue_arg.patchset, self.GetIssue()))
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002766
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002767 remote_url = self.GetRemoteUrl()
2768 if remote_url.endswith('.git'):
2769 remote_url = remote_url[:-len('.git')]
2770 remote_url = remote_url.rstrip('/')
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002771
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002772 fetch_info = revision_info['fetch']['http']
2773 fetch_info['url'] = fetch_info['url'].rstrip('/')
Josip Sokcevicf2cfd3d2021-03-30 18:39:18 +00002774
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002775 if remote_url != fetch_info['url']:
2776 DieWithError(
2777 'Trying to patch a change from %s but this repo appears '
2778 'to be %s.' % (fetch_info['url'], remote_url))
Gavin Mak4e5e3992022-11-14 22:40:12 +00002779
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002780 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002781
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002782 # Set issue immediately in case the cherry-pick fails, which happens
2783 # when resolving conflicts.
2784 if self.GetBranch():
2785 self.SetIssue(parsed_issue_arg.issue)
tandrii88189772016-09-29 04:29:57 -07002786
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002787 if force:
2788 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2789 print('Checked out commit for change %i patchset %i locally' %
2790 (parsed_issue_arg.issue, patchset))
2791 elif nocommit:
2792 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2793 print('Patch applied to index.')
2794 else:
2795 RunGit(['cherry-pick', 'FETCH_HEAD'])
2796 print('Committed patch for change %i patchset %i locally.' %
2797 (parsed_issue_arg.issue, patchset))
2798 print(
2799 'Note: this created a local commit which does not have '
2800 'the same hash as the one uploaded for review. This will make '
2801 'uploading changes based on top of this branch difficult.\n'
2802 'If you want to do that, use "git cl patch --force" instead.')
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002803
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002804 if self.GetBranch():
2805 self.SetPatchset(patchset)
2806 fetched_hash = scm.GIT.ResolveCommit(settings.GetRoot(),
2807 'FETCH_HEAD')
2808 self._GitSetBranchConfigValue(LAST_UPLOAD_HASH_CONFIG_KEY,
2809 fetched_hash)
2810 self._GitSetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY,
2811 fetched_hash)
2812 else:
2813 print(
2814 'WARNING: You are in detached HEAD state.\n'
2815 'The patch has been applied to your checkout, but you will not be '
2816 'able to upload a new patch set to the gerrit issue.\n'
2817 'Try using the \'-b\' option if you would like to work on a '
2818 'branch and/or upload a new patch set.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002819
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002820 return 0
2821
2822 @staticmethod
2823 def _GerritCommitMsgHookCheck(offer_removal):
2824 # type: (bool) -> None
2825 """Checks for the gerrit's commit-msg hook and removes it if necessary."""
2826 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2827 if not os.path.exists(hook):
2828 return
2829 # Crude attempt to distinguish Gerrit Codereview hook from a potentially
2830 # custom developer-made one.
2831 data = gclient_utils.FileRead(hook)
2832 if not ('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2833 return
2834 print('WARNING: You have Gerrit commit-msg hook installed.\n'
2835 'It is not necessary for uploading with git cl in squash mode, '
2836 'and may interfere with it in subtle ways.\n'
2837 'We recommend you remove the commit-msg hook.')
2838 if offer_removal:
2839 if ask_for_explicit_yes('Do you want to remove it now?'):
2840 gclient_utils.rm_file_or_tree(hook)
2841 print('Gerrit commit-msg hook removed.')
2842 else:
2843 print('OK, will keep Gerrit commit-msg hook in place.')
2844
2845 def _CleanUpOldTraces(self):
2846 """Keep only the last |MAX_TRACES| traces."""
2847 try:
2848 traces = sorted([
2849 os.path.join(TRACES_DIR, f) for f in os.listdir(TRACES_DIR)
2850 if (os.path.isfile(os.path.join(TRACES_DIR, f))
2851 and not f.startswith('tmp'))
2852 ])
2853 traces_to_delete = traces[:-MAX_TRACES]
2854 for trace in traces_to_delete:
2855 os.remove(trace)
2856 except OSError:
2857 print('WARNING: Failed to remove old git traces from\n'
2858 ' %s'
2859 'Consider removing them manually.' % TRACES_DIR)
2860
2861 def _WriteGitPushTraces(self, trace_name, traces_dir, git_push_metadata):
2862 """Zip and write the git push traces stored in traces_dir."""
2863 gclient_utils.safe_makedirs(TRACES_DIR)
2864 traces_zip = trace_name + '-traces'
2865 traces_readme = trace_name + '-README'
2866 # Create a temporary dir to store git config and gitcookies in. It will
2867 # be compressed and stored next to the traces.
2868 git_info_dir = tempfile.mkdtemp()
2869 git_info_zip = trace_name + '-git-info'
2870
2871 git_push_metadata['now'] = datetime_now().strftime(
2872 '%Y-%m-%dT%H:%M:%S.%f')
2873
2874 git_push_metadata['trace_name'] = trace_name
2875 gclient_utils.FileWrite(traces_readme,
2876 TRACES_README_FORMAT % git_push_metadata)
2877
2878 # Keep only the first 6 characters of the git hashes on the packet
2879 # trace. This greatly decreases size after compression.
2880 packet_traces = os.path.join(traces_dir, 'trace-packet')
2881 if os.path.isfile(packet_traces):
2882 contents = gclient_utils.FileRead(packet_traces)
2883 gclient_utils.FileWrite(packet_traces,
2884 GIT_HASH_RE.sub(r'\1', contents))
2885 shutil.make_archive(traces_zip, 'zip', traces_dir)
2886
2887 # Collect and compress the git config and gitcookies.
2888 git_config = RunGit(['config', '-l'])
2889 gclient_utils.FileWrite(os.path.join(git_info_dir, 'git-config'),
2890 git_config)
2891
2892 cookie_auth = gerrit_util.Authenticator.get()
2893 if isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
2894 gitcookies_path = cookie_auth.get_gitcookies_path()
2895 if os.path.isfile(gitcookies_path):
2896 gitcookies = gclient_utils.FileRead(gitcookies_path)
2897 gclient_utils.FileWrite(
2898 os.path.join(git_info_dir, 'gitcookies'),
2899 GITCOOKIES_REDACT_RE.sub('REDACTED', gitcookies))
2900 shutil.make_archive(git_info_zip, 'zip', git_info_dir)
2901
2902 gclient_utils.rmtree(git_info_dir)
2903
2904 def _RunGitPushWithTraces(self,
2905 refspec,
2906 refspec_opts,
2907 git_push_metadata,
2908 git_push_options=None):
2909 """Run git push and collect the traces resulting from the execution."""
2910 # Create a temporary directory to store traces in. Traces will be
2911 # compressed and stored in a 'traces' dir inside depot_tools.
2912 traces_dir = tempfile.mkdtemp()
2913 trace_name = os.path.join(TRACES_DIR,
2914 datetime_now().strftime('%Y%m%dT%H%M%S.%f'))
2915
2916 env = os.environ.copy()
2917 env['GIT_REDACT_COOKIES'] = 'o,SSO,GSSO_Uberproxy'
2918 env['GIT_TR2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
2919 env['GIT_TRACE2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
2920 env['GIT_TRACE_CURL'] = os.path.join(traces_dir, 'trace-curl')
2921 env['GIT_TRACE_CURL_NO_DATA'] = '1'
2922 env['GIT_TRACE_PACKET'] = os.path.join(traces_dir, 'trace-packet')
2923
2924 push_returncode = 0
2925 before_push = time_time()
2926 try:
2927 remote_url = self.GetRemoteUrl()
2928 push_cmd = ['git', 'push', remote_url, refspec]
2929 if git_push_options:
2930 for opt in git_push_options:
2931 push_cmd.extend(['-o', opt])
2932
2933 push_stdout = gclient_utils.CheckCallAndFilter(
2934 push_cmd,
2935 env=env,
2936 print_stdout=True,
2937 # Flush after every line: useful for seeing progress when
2938 # running as recipe.
2939 filter_fn=lambda _: sys.stdout.flush())
2940 push_stdout = push_stdout.decode('utf-8', 'replace')
2941 except subprocess2.CalledProcessError as e:
2942 push_returncode = e.returncode
2943 if 'blocked keyword' in str(e.stdout) or 'banned word' in str(
2944 e.stdout):
2945 raise GitPushError(
2946 'Failed to create a change, very likely due to blocked keyword. '
2947 'Please examine output above for the reason of the failure.\n'
2948 'If this is a false positive, you can try to bypass blocked '
2949 'keyword by using push option '
2950 '-o banned-words~skip, e.g.:\n'
2951 'git cl upload -o banned-words~skip\n\n'
2952 'If git-cl is not working correctly, file a bug under the '
2953 'Infra>SDK component.')
2954 if 'git push -o nokeycheck' in str(e.stdout):
2955 raise GitPushError(
2956 'Failed to create a change, very likely due to a private key being '
2957 'detected. Please examine output above for the reason of the '
2958 'failure.\n'
2959 'If this is a false positive, you can try to bypass private key '
2960 'detection by using push option '
2961 '-o nokeycheck, e.g.:\n'
2962 'git cl upload -o nokeycheck\n\n'
2963 'If git-cl is not working correctly, file a bug under the '
2964 'Infra>SDK component.')
2965
2966 raise GitPushError(
2967 'Failed to create a change. Please examine output above for the '
2968 'reason of the failure.\n'
2969 'For emergencies, Googlers can escalate to '
2970 'go/gob-support or go/notify#gob\n'
2971 'Hint: run command below to diagnose common Git/Gerrit '
2972 'credential problems:\n'
2973 ' git cl creds-check\n'
2974 '\n'
2975 'If git-cl is not working correctly, file a bug under the Infra>SDK '
2976 'component including the files below.\n'
2977 'Review the files before upload, since they might contain sensitive '
2978 'information.\n'
2979 'Set the Restrict-View-Google label so that they are not publicly '
2980 'accessible.\n' + TRACES_MESSAGE % {'trace_name': trace_name})
2981 finally:
2982 execution_time = time_time() - before_push
2983 metrics.collector.add_repeated(
2984 'sub_commands', {
2985 'command':
2986 'git push',
2987 'execution_time':
2988 execution_time,
2989 'exit_code':
2990 push_returncode,
2991 'arguments':
2992 metrics_utils.extract_known_subcommand_args(refspec_opts),
2993 })
2994
2995 git_push_metadata['execution_time'] = execution_time
2996 git_push_metadata['exit_code'] = push_returncode
2997 self._WriteGitPushTraces(trace_name, traces_dir, git_push_metadata)
2998
2999 self._CleanUpOldTraces()
3000 gclient_utils.rmtree(traces_dir)
3001
3002 return push_stdout
3003
3004 def CMDUploadChange(self, options, git_diff_args, custom_cl_base,
3005 change_desc):
3006 """Upload the current branch to Gerrit, retry if new remote HEAD is
3007 found. options and change_desc may be mutated."""
3008 remote, remote_branch = self.GetRemoteBranch()
3009 branch = GetTargetRef(remote, remote_branch, options.target_branch)
3010
3011 try:
3012 return self._CMDUploadChange(options, git_diff_args, custom_cl_base,
3013 change_desc, branch)
3014 except GitPushError as e:
3015 # Repository might be in the middle of transition to main branch as
3016 # default, and uploads to old default might be blocked.
3017 if remote_branch not in [DEFAULT_OLD_BRANCH, DEFAULT_NEW_BRANCH]:
3018 DieWithError(str(e), change_desc)
3019
3020 project_head = gerrit_util.GetProjectHead(self._gerrit_host,
3021 self.GetGerritProject())
3022 if project_head == branch:
3023 DieWithError(str(e), change_desc)
3024 branch = project_head
3025
3026 print("WARNING: Fetching remote state and retrying upload to default "
3027 "branch...")
3028 RunGit(['fetch', '--prune', remote])
3029 options.edit_description = False
3030 options.force = True
3031 try:
3032 self._CMDUploadChange(options, git_diff_args, custom_cl_base,
3033 change_desc, branch)
3034 except GitPushError as e:
3035 DieWithError(str(e), change_desc)
3036
3037 def _CMDUploadChange(self, options, git_diff_args, custom_cl_base,
3038 change_desc, branch):
3039 """Upload the current branch to Gerrit."""
3040 if options.squash:
3041 Changelist._GerritCommitMsgHookCheck(
3042 offer_removal=not options.force)
3043 external_parent = None
3044 if self.GetIssue():
3045 # User requested to change description
3046 if options.edit_description:
3047 change_desc.prompt()
3048 change_detail = self._GetChangeDetail(['CURRENT_REVISION'])
3049 change_id = change_detail['change_id']
3050 change_desc.ensure_change_id(change_id)
3051
3052 # Check if changes outside of this workspace have been uploaded.
3053 current_rev = change_detail['current_revision']
3054 last_uploaded_rev = self._GitGetBranchConfigValue(
3055 GERRIT_SQUASH_HASH_CONFIG_KEY)
3056 if last_uploaded_rev and current_rev != last_uploaded_rev:
3057 external_parent = self._UpdateWithExternalChanges()
3058 else: # if not self.GetIssue()
3059 if not options.force and not options.message_file:
3060 change_desc.prompt()
3061 change_ids = git_footers.get_footer_change_id(
3062 change_desc.description)
3063 if len(change_ids) == 1:
3064 change_id = change_ids[0]
3065 else:
3066 change_id = GenerateGerritChangeId(change_desc.description)
3067 change_desc.ensure_change_id(change_id)
3068
3069 if options.preserve_tryjobs:
3070 change_desc.set_preserve_tryjobs()
3071
3072 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
3073 parent = external_parent or self._ComputeParent(
3074 remote, upstream_branch, custom_cl_base, options.force,
3075 change_desc)
3076 tree = RunGit(['rev-parse', 'HEAD:']).strip()
3077 with gclient_utils.temporary_file() as desc_tempfile:
3078 gclient_utils.FileWrite(desc_tempfile, change_desc.description)
3079 ref_to_push = RunGit(
3080 ['commit-tree', tree, '-p', parent, '-F',
3081 desc_tempfile]).strip()
3082 else: # if not options.squash
3083 if options.no_add_changeid:
3084 pass
3085 else: # adding Change-Ids is okay.
3086 if not git_footers.get_footer_change_id(
3087 change_desc.description):
3088 DownloadGerritHook(False)
3089 change_desc.set_description(
3090 self._AddChangeIdToCommitMessage(
3091 change_desc.description, git_diff_args))
3092 ref_to_push = 'HEAD'
3093 # For no-squash mode, we assume the remote called "origin" is the
3094 # one we want. It is not worthwhile to support different workflows
3095 # for no-squash mode.
3096 parent = 'origin/%s' % branch
3097 # attempt to extract the changeid from the current description
3098 # fail informatively if not possible.
3099 change_id_candidates = git_footers.get_footer_change_id(
3100 change_desc.description)
3101 if not change_id_candidates:
3102 DieWithError("Unable to extract change-id from message.")
3103 change_id = change_id_candidates[0]
3104
3105 SaveDescriptionBackup(change_desc)
3106 commits = RunGitSilent(['rev-list',
3107 '%s..%s' % (parent, ref_to_push)]).splitlines()
3108 if len(commits) > 1:
3109 print(
3110 'WARNING: This will upload %d commits. Run the following command '
3111 'to see which commits will be uploaded: ' % len(commits))
3112 print('git log %s..%s' % (parent, ref_to_push))
3113 print('You can also use `git squash-branch` to squash these into a '
3114 'single commit.')
3115 confirm_or_exit(action='upload')
3116
3117 reviewers = sorted(change_desc.get_reviewers())
3118 cc = []
3119 # Add default, watchlist, presubmit ccs if this is the initial upload
3120 # and CL is not private and auto-ccing has not been disabled.
3121 if not options.private and not options.no_autocc and not self.GetIssue(
3122 ):
3123 cc = self.GetCCList().split(',')
3124 if len(cc) > 100:
3125 lsc = ('https://chromium.googlesource.com/chromium/src/+/HEAD/docs/'
3126 'process/lsc/lsc_workflow.md')
3127 print('WARNING: This will auto-CC %s users.' % len(cc))
3128 print('LSC may be more appropriate: %s' % lsc)
3129 print('You can also use the --no-autocc flag to disable auto-CC.')
3130 confirm_or_exit(action='continue')
3131 # Add cc's from the --cc flag.
3132 if options.cc:
3133 cc.extend(options.cc)
3134 cc = [email.strip() for email in cc if email.strip()]
3135 if change_desc.get_cced():
3136 cc.extend(change_desc.get_cced())
3137 if self.GetGerritHost() == 'chromium-review.googlesource.com':
3138 valid_accounts = set(reviewers + cc)
3139 # TODO(crbug/877717): relax this for all hosts.
3140 else:
3141 valid_accounts = gerrit_util.ValidAccounts(self.GetGerritHost(),
3142 reviewers + cc)
3143 logging.info('accounts %s are recognized, %s invalid',
3144 sorted(valid_accounts),
3145 set(reviewers + cc).difference(set(valid_accounts)))
3146
3147 # Extra options that can be specified at push time. Doc:
3148 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
3149 refspec_opts = self._GetRefSpecOptions(options, change_desc)
3150
3151 for r in sorted(reviewers):
3152 if r in valid_accounts:
3153 refspec_opts.append('r=%s' % r)
3154 reviewers.remove(r)
3155 else:
3156 # TODO(tandrii): this should probably be a hard failure.
3157 print(
3158 'WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
3159 % r)
3160 for c in sorted(cc):
3161 # refspec option will be rejected if cc doesn't correspond to an
3162 # account, even though REST call to add such arbitrary cc may
3163 # succeed.
3164 if c in valid_accounts:
3165 refspec_opts.append('cc=%s' % c)
3166 cc.remove(c)
3167
3168 refspec_suffix = ''
3169 if refspec_opts:
3170 refspec_suffix = '%' + ','.join(refspec_opts)
3171 assert ' ' not in refspec_suffix, (
3172 'spaces not allowed in refspec: "%s"' % refspec_suffix)
3173 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
3174
3175 git_push_metadata = {
3176 'gerrit_host': self.GetGerritHost(),
3177 'title': options.title or '<untitled>',
3178 'change_id': change_id,
3179 'description': change_desc.description,
3180 }
3181
3182 # Gerrit may or may not update fast enough to return the correct
3183 # patchset number after we push. Get the pre-upload patchset and
3184 # increment later.
3185 latest_ps = self.GetMostRecentPatchset(update=False) or 0
3186
3187 push_stdout = self._RunGitPushWithTraces(refspec, refspec_opts,
3188 git_push_metadata,
3189 options.push_options)
3190
3191 if options.squash:
3192 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
3193 change_numbers = [
3194 m.group(1) for m in map(regex.match, push_stdout.splitlines())
3195 if m
3196 ]
3197 if len(change_numbers) != 1:
3198 DieWithError((
3199 'Created|Updated %d issues on Gerrit, but only 1 expected.\n'
3200 'Change-Id: %s') % (len(change_numbers), change_id),
3201 change_desc)
3202 self.SetIssue(change_numbers[0])
3203 self.SetPatchset(latest_ps + 1)
3204 self._GitSetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY,
3205 ref_to_push)
3206
3207 if self.GetIssue() and (reviewers or cc):
3208 # GetIssue() is not set in case of non-squash uploads according to
3209 # tests. TODO(crbug.com/751901): non-squash uploads in git cl should
3210 # be removed.
3211 gerrit_util.AddReviewers(self.GetGerritHost(),
3212 self._GerritChangeIdentifier(),
3213 reviewers,
3214 cc,
3215 notify=bool(options.send_mail))
3216
3217 return 0
3218
3219 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
3220 change_desc):
3221 """Computes parent of the generated commit to be uploaded to Gerrit.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003222
3223 Returns revision or a ref name.
3224 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003225 if custom_cl_base:
3226 # Try to avoid creating additional unintended CLs when uploading,
3227 # unless user wants to take this risk.
3228 local_ref_of_target_remote = self.GetRemoteBranch()[1]
3229 code, _ = RunGitWithCode([
3230 'merge-base', '--is-ancestor', custom_cl_base,
3231 local_ref_of_target_remote
3232 ])
3233 if code == 1:
3234 print(
3235 '\nWARNING: Manually specified base of this CL `%s` '
3236 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
3237 'If you proceed with upload, more than 1 CL may be created by '
3238 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
3239 'If you are certain that specified base `%s` has already been '
3240 'uploaded to Gerrit as another CL, you may proceed.\n' %
3241 (custom_cl_base, local_ref_of_target_remote,
3242 custom_cl_base))
3243 if not force:
3244 confirm_or_exit(
3245 'Do you take responsibility for cleaning up potential mess '
3246 'resulting from proceeding with upload?',
3247 action='upload')
3248 return custom_cl_base
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003249
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003250 if remote != '.':
3251 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003252
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003253 # If our upstream branch is local, we base our squashed commit on its
3254 # squashed version.
3255 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
Aaron Gablef97e33d2017-03-30 15:44:27 -07003256
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003257 if upstream_branch_name == 'master':
3258 return self.GetCommonAncestorWithUpstream()
3259 if upstream_branch_name == 'main':
3260 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003261
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003262 # Check the squashed hash of the parent.
3263 # TODO(tandrii): consider checking parent change in Gerrit and using its
3264 # hash if tree hash of latest parent revision (patchset) in Gerrit
3265 # matches the tree hash of the parent branch. The upside is less likely
3266 # bogus requests to reupload parent change just because it's uploadhash
3267 # is missing, yet the downside likely exists, too (albeit unknown to me
3268 # yet).
3269 parent = scm.GIT.GetBranchConfig(settings.GetRoot(),
3270 upstream_branch_name,
3271 GERRIT_SQUASH_HASH_CONFIG_KEY)
3272 # Verify that the upstream branch has been uploaded too, otherwise
3273 # Gerrit will create additional CLs when uploading.
3274 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
3275 RunGitSilent(['rev-parse', parent + ':'])):
3276 DieWithError(
3277 '\nUpload upstream branch %s first.\n'
3278 'It is likely that this branch has been rebased since its last '
3279 'upload, so you just need to upload it again.\n'
3280 '(If you uploaded it with --no-squash, then branch dependencies '
3281 'are not supported, and you should reupload with --squash.)' %
3282 upstream_branch_name, change_desc)
3283 return parent
Aaron Gablef97e33d2017-03-30 15:44:27 -07003284
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003285 def _UpdateWithExternalChanges(self):
3286 """Updates workspace with external changes.
Gavin Mak4e5e3992022-11-14 22:40:12 +00003287
3288 Returns the commit hash that should be used as the merge base on upload.
3289 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003290 local_ps = self.GetPatchset()
3291 if local_ps is None:
3292 return
Gavin Mak4e5e3992022-11-14 22:40:12 +00003293
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003294 external_ps = self.GetMostRecentPatchset(update=False)
3295 if external_ps is None or local_ps == external_ps or \
3296 not self._IsPatchsetRangeSignificant(local_ps + 1, external_ps):
3297 return
Gavin Mak4e5e3992022-11-14 22:40:12 +00003298
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003299 num_changes = external_ps - local_ps
3300 if num_changes > 1:
3301 change_words = 'changes were'
3302 else:
3303 change_words = 'change was'
3304 print('\n%d external %s published to %s:\n' %
3305 (num_changes, change_words, self.GetIssueURL(short=True)))
Gavin Mak6f905472023-01-06 21:01:36 +00003306
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003307 # Print an overview of external changes.
3308 ps_to_commit = {}
3309 ps_to_info = {}
3310 revisions = self._GetChangeDetail(['ALL_REVISIONS'])
3311 for commit_id, revision_info in revisions.get('revisions', {}).items():
3312 ps_num = revision_info['_number']
3313 ps_to_commit[ps_num] = commit_id
3314 ps_to_info[ps_num] = revision_info
Gavin Mak6f905472023-01-06 21:01:36 +00003315
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003316 for ps in range(external_ps, local_ps, -1):
3317 commit = ps_to_commit[ps][:8]
3318 desc = ps_to_info[ps].get('description', '')
3319 print('Patchset %d [%s] %s' % (ps, commit, desc))
Gavin Mak6f905472023-01-06 21:01:36 +00003320
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003321 print('\nSee diff at: %s/%d..%d' %
3322 (self.GetIssueURL(short=True), local_ps, external_ps))
3323 print('\nUploading without applying patches will override them.')
Josip Sokcevic43ceaf02023-05-25 15:56:00 +00003324
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003325 if not ask_for_explicit_yes('Get the latest changes and apply on top?'):
3326 return
Gavin Mak4e5e3992022-11-14 22:40:12 +00003327
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003328 # Get latest Gerrit merge base. Use the first parent even if multiple
3329 # exist.
3330 external_parent = self._GetChangeCommit(
3331 revision=external_ps)['parents'][0]
3332 external_base = external_parent['commit']
Gavin Mak4e5e3992022-11-14 22:40:12 +00003333
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003334 branch = git_common.current_branch()
3335 local_base = self.GetCommonAncestorWithUpstream()
3336 if local_base != external_base:
3337 print('\nLocal merge base %s is different from Gerrit %s.\n' %
3338 (local_base, external_base))
3339 if git_common.upstream(branch):
3340 confirm_or_exit(
3341 'Can\'t apply the latest changes from Gerrit.\n'
3342 'Continue with upload and override the latest changes?')
3343 return
3344 print(
3345 'No upstream branch set. Continuing upload with Gerrit merge base.'
3346 )
Gavin Mak4e5e3992022-11-14 22:40:12 +00003347
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003348 external_parent_last_uploaded = self._GetChangeCommit(
3349 revision=local_ps)['parents'][0]
3350 external_base_last_uploaded = external_parent_last_uploaded['commit']
Josip Sokcevic2568d4c2023-05-24 03:37:42 +00003351
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003352 if external_base != external_base_last_uploaded:
3353 print('\nPatch set merge bases are different (%s, %s).\n' %
3354 (external_base_last_uploaded, external_base))
3355 confirm_or_exit(
3356 'Can\'t apply the latest changes from Gerrit.\n'
3357 'Continue with upload and override the latest changes?')
3358 return
Josip Sokcevic2568d4c2023-05-24 03:37:42 +00003359
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003360 # Fetch Gerrit's CL base if it doesn't exist locally.
3361 remote, _ = self.GetRemoteBranch()
3362 if not scm.GIT.IsValidRevision(settings.GetRoot(), external_base):
3363 RunGitSilent(['fetch', remote, external_base])
Gavin Mak4e5e3992022-11-14 22:40:12 +00003364
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003365 # Get the diff between local_ps and external_ps.
3366 print('Fetching changes...')
3367 issue = self.GetIssue()
3368 changes_ref = 'refs/changes/%02d/%d/' % (issue % 100, issue)
3369 RunGitSilent(['fetch', remote, changes_ref + str(local_ps)])
3370 last_uploaded = RunGitSilent(['rev-parse', 'FETCH_HEAD']).strip()
3371 RunGitSilent(['fetch', remote, changes_ref + str(external_ps)])
3372 latest_external = RunGitSilent(['rev-parse', 'FETCH_HEAD']).strip()
Josip Sokcevic2568d4c2023-05-24 03:37:42 +00003373
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003374 # If the commit parents are different, don't apply the diff as it very
3375 # likely contains many more changes not relevant to this CL.
3376 parents = RunGitSilent(
3377 ['rev-parse',
3378 '%s~1' % (last_uploaded),
3379 '%s~1' % (latest_external)]).strip().split()
3380 assert len(parents) == 2, 'Expected two parents.'
3381 if parents[0] != parents[1]:
3382 confirm_or_exit(
3383 'Can\'t apply the latest changes from Gerrit (parent mismatch '
3384 'between PS).\n'
3385 'Continue with upload and override the latest changes?')
3386 return
Josip Sokcevic2568d4c2023-05-24 03:37:42 +00003387
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003388 diff = RunGitSilent(
3389 ['diff', '%s..%s' % (last_uploaded, latest_external)])
Gavin Mak4e5e3992022-11-14 22:40:12 +00003390
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003391 # Diff can be empty in the case of trivial rebases.
3392 if not diff:
3393 return external_base
Gavin Mak4e5e3992022-11-14 22:40:12 +00003394
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003395 # Apply the diff.
3396 with gclient_utils.temporary_file() as diff_tempfile:
3397 gclient_utils.FileWrite(diff_tempfile, diff)
3398 clean_patch = RunGitWithCode(['apply', '--check',
3399 diff_tempfile])[0] == 0
3400 RunGitSilent(['apply', '-3', '--intent-to-add', diff_tempfile])
3401 if not clean_patch:
3402 # Normally patchset is set after upload. But because we exit,
3403 # that never happens. Updating here makes sure that subsequent
3404 # uploads don't need to fetch/apply the same diff again.
3405 self.SetPatchset(external_ps)
3406 DieWithError(
3407 '\nPatch did not apply cleanly. Please resolve any '
3408 'conflicts and reupload.')
Gavin Mak4e5e3992022-11-14 22:40:12 +00003409
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003410 message = 'Incorporate external changes from '
3411 if num_changes == 1:
3412 message += 'patchset %d' % external_ps
3413 else:
3414 message += 'patchsets %d to %d' % (local_ps + 1, external_ps)
3415 RunGitSilent(['commit', '-am', message])
3416 # TODO(crbug.com/1382528): Use the previous commit's message as a
3417 # default patchset title instead of this 'Incorporate' message.
3418 return external_base
Gavin Mak4e5e3992022-11-14 22:40:12 +00003419
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003420 def _AddChangeIdToCommitMessage(self, log_desc, args):
3421 """Re-commits using the current message, assumes the commit hook is in
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003422 place.
3423 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003424 RunGit(['commit', '--amend', '-m', log_desc])
3425 new_log_desc = _create_description_from_log(args)
3426 if git_footers.get_footer_change_id(new_log_desc):
3427 print('git-cl: Added Change-Id to commit message.')
3428 return new_log_desc
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00003429
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003430 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003431
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003432 def CannotTriggerTryJobReason(self):
3433 try:
3434 data = self._GetChangeDetail()
3435 except GerritChangeNotExists:
3436 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003437
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003438 if data['status'] in ('ABANDONED', 'MERGED'):
3439 return 'CL %s is closed' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003440
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003441 def GetGerritChange(self, patchset=None):
3442 """Returns a buildbucket.v2.GerritChange message for the current issue."""
3443 host = urllib.parse.urlparse(self.GetCodereviewServer()).hostname
3444 issue = self.GetIssue()
3445 patchset = int(patchset or self.GetPatchset())
3446 data = self._GetChangeDetail(['ALL_REVISIONS'])
Edward Lemurd4d1ba42019-09-20 21:46:37 +00003447
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003448 assert host and issue and patchset, 'CL must be uploaded first'
Edward Lemurd4d1ba42019-09-20 21:46:37 +00003449
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003450 has_patchset = any(
3451 int(revision_data['_number']) == patchset
3452 for revision_data in data['revisions'].values())
3453 if not has_patchset:
3454 raise Exception('Patchset %d is not known in Gerrit change %d' %
3455 (patchset, self.GetIssue()))
Edward Lemurd4d1ba42019-09-20 21:46:37 +00003456
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003457 return {
3458 'host': host,
3459 'change': issue,
3460 'project': data['project'],
3461 'patchset': patchset,
3462 }
tandriie113dfd2016-10-11 10:20:12 -07003463
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003464 def GetIssueOwner(self):
3465 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003466
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003467 def GetReviewers(self):
3468 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
3469 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01003470
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003471
Lei Zhang8a0efc12020-08-05 19:58:45 +00003472def _get_bug_line_values(default_project_prefix, bugs):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003473 """Given default_project_prefix and comma separated list of bugs, yields bug
Lei Zhang8a0efc12020-08-05 19:58:45 +00003474 line values.
tandriif9aefb72016-07-01 09:06:51 -07003475
3476 Each bug can be either:
Lei Zhang8a0efc12020-08-05 19:58:45 +00003477 * a number, which is combined with default_project_prefix
tandriif9aefb72016-07-01 09:06:51 -07003478 * string, which is left as is.
3479
3480 This function may produce more than one line, because bugdroid expects one
3481 project per line.
3482
Lei Zhang8a0efc12020-08-05 19:58:45 +00003483 >>> list(_get_bug_line_values('v8:', '123,chromium:789'))
tandriif9aefb72016-07-01 09:06:51 -07003484 ['v8:123', 'chromium:789']
3485 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003486 default_bugs = []
3487 others = []
3488 for bug in bugs.split(','):
3489 bug = bug.strip()
3490 if bug:
3491 try:
3492 default_bugs.append(int(bug))
3493 except ValueError:
3494 others.append(bug)
tandriif9aefb72016-07-01 09:06:51 -07003495
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003496 if default_bugs:
3497 default_bugs = ','.join(map(str, default_bugs))
3498 if default_project_prefix:
3499 if not default_project_prefix.endswith(':'):
3500 default_project_prefix += ':'
3501 yield '%s%s' % (default_project_prefix, default_bugs)
3502 else:
3503 yield default_bugs
3504 for other in sorted(others):
3505 # Don't bother finding common prefixes, CLs with >2 bugs are very very
3506 # rare.
3507 yield other
tandriif9aefb72016-07-01 09:06:51 -07003508
3509
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003510def FindCodereviewSettingsFile(filename='codereview.settings'):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003511 """Finds the given file starting in the cwd and going up.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003512
3513 Only looks up to the top of the repository unless an
3514 'inherit-review-settings-ok' file exists in the root of the repository.
3515 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003516 inherit_ok_file = 'inherit-review-settings-ok'
3517 cwd = os.getcwd()
3518 root = settings.GetRoot()
3519 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3520 root = None
3521 while True:
3522 if os.path.isfile(os.path.join(cwd, filename)):
3523 return open(os.path.join(cwd, filename))
3524 if cwd == root:
3525 break
3526 parent_dir = os.path.dirname(cwd)
3527 if parent_dir == cwd:
3528 # We hit the system root directory.
3529 break
3530 cwd = parent_dir
3531 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003532
3533
3534def LoadCodereviewSettingsFromFile(fileobj):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003535 """Parses a codereview.settings file and updates hooks."""
3536 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003537
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003538 def SetProperty(name, setting, unset_error_ok=False):
3539 fullname = 'rietveld.' + name
3540 if setting in keyvals:
3541 RunGit(['config', fullname, keyvals[setting]])
3542 else:
3543 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003544
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003545 if not keyvals.get('GERRIT_HOST', False):
3546 SetProperty('server', 'CODE_REVIEW_SERVER')
3547 # Only server setting is required. Other settings can be absent.
3548 # In that case, we ignore errors raised during option deletion attempt.
3549 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
3550 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3551 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
3552 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
3553 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3554 SetProperty('cpplint-ignore-regex',
3555 'LINT_IGNORE_REGEX',
3556 unset_error_ok=True)
3557 SetProperty('run-post-upload-hook',
3558 'RUN_POST_UPLOAD_HOOK',
3559 unset_error_ok=True)
3560 SetProperty('format-full-by-default',
3561 'FORMAT_FULL_BY_DEFAULT',
3562 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003563
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003564 if 'GERRIT_HOST' in keyvals:
3565 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003566
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003567 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
3568 RunGit([
3569 'config', 'gerrit.squash-uploads', keyvals['GERRIT_SQUASH_UPLOADS']
3570 ])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003571
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003572 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
3573 RunGit([
3574 'config', 'gerrit.skip-ensure-authenticated',
3575 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']
3576 ])
tandrii@chromium.org28253532016-04-14 13:46:56 +00003577
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003578 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3579 # should be of the form
3580 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3581 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
3582 RunGit([
3583 'config', keyvals['PUSH_URL_CONFIG'], keyvals['ORIGIN_URL_CONFIG']
3584 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003585
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003586
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003587def urlretrieve(source, destination):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003588 """Downloads a network object to a local file, like urllib.urlretrieve.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003589
3590 This is necessary because urllib is broken for SSL connections via a proxy.
3591 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003592 with open(destination, 'wb') as f:
3593 f.write(urllib.request.urlopen(source).read())
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003594
3595
ukai@chromium.org712d6102013-11-27 00:52:58 +00003596def hasSheBang(fname):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003597 """Checks fname is a #! script."""
3598 with open(fname) as f:
3599 return f.read(2).startswith('#!')
ukai@chromium.org712d6102013-11-27 00:52:58 +00003600
3601
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003602def DownloadGerritHook(force):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003603 """Downloads and installs a Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003604
3605 Args:
3606 force: True to update hooks. False to install hooks if not present.
3607 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003608 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
3609 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3610 if not os.access(dst, os.X_OK):
3611 if os.path.exists(dst):
3612 if not force:
3613 return
3614 try:
3615 urlretrieve(src, dst)
3616 if not hasSheBang(dst):
3617 DieWithError('Not a script: %s\n'
3618 'You need to download from\n%s\n'
3619 'into .git/hooks/commit-msg and '
3620 'chmod +x .git/hooks/commit-msg' % (dst, src))
3621 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3622 except Exception:
3623 if os.path.exists(dst):
3624 os.remove(dst)
3625 DieWithError('\nFailed to download hooks.\n'
3626 'You need to download from\n%s\n'
3627 'into .git/hooks/commit-msg and '
3628 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003629
3630
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003631class _GitCookiesChecker(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003632 """Provides facilities for validating and suggesting fixes to .gitcookies."""
3633 def __init__(self):
3634 # Cached list of [host, identity, source], where source is either
3635 # .gitcookies or .netrc.
3636 self._all_hosts = None
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003637
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003638 def ensure_configured_gitcookies(self):
3639 """Runs checks and suggests fixes to make git use .gitcookies from default
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003640 path."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003641 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3642 configured_path = RunGitSilent(
3643 ['config', '--global', 'http.cookiefile']).strip()
3644 configured_path = os.path.expanduser(configured_path)
3645 if configured_path:
3646 self._ensure_default_gitcookies_path(configured_path, default)
3647 else:
3648 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003649
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003650 @staticmethod
3651 def _ensure_default_gitcookies_path(configured_path, default_path):
3652 assert configured_path
3653 if configured_path == default_path:
3654 print('git is already configured to use your .gitcookies from %s' %
3655 configured_path)
3656 return
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003657
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003658 print('WARNING: You have configured custom path to .gitcookies: %s\n'
3659 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3660 (configured_path, default_path))
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003661
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003662 if not os.path.exists(configured_path):
3663 print('However, your configured .gitcookies file is missing.')
3664 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3665 action='reconfigure')
3666 RunGit(['config', '--global', 'http.cookiefile', default_path])
3667 return
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003668
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003669 if os.path.exists(default_path):
3670 print('WARNING: default .gitcookies file already exists %s' %
3671 default_path)
3672 DieWithError(
3673 'Please delete %s manually and re-run git cl creds-check' %
3674 default_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003675
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003676 confirm_or_exit('Move existing .gitcookies to default location?',
3677 action='move')
3678 shutil.move(configured_path, default_path)
3679 RunGit(['config', '--global', 'http.cookiefile', default_path])
3680 print('Moved and reconfigured git to use .gitcookies from %s' %
3681 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003682
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003683 @staticmethod
3684 def _configure_gitcookies_path(default_path):
3685 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3686 if os.path.exists(netrc_path):
3687 print(
3688 'You seem to be using outdated .netrc for git credentials: %s' %
3689 netrc_path)
3690 print(
3691 'This tool will guide you through setting up recommended '
3692 '.gitcookies store for git credentials.\n'
3693 '\n'
3694 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3695 ' git config --global --unset http.cookiefile\n'
3696 ' mv %s %s.backup\n\n' % (default_path, default_path))
3697 confirm_or_exit(action='setup .gitcookies')
3698 RunGit(['config', '--global', 'http.cookiefile', default_path])
3699 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003700
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003701 def get_hosts_with_creds(self, include_netrc=False):
3702 if self._all_hosts is None:
3703 a = gerrit_util.CookiesAuthenticator()
3704 self._all_hosts = [(h, u, s) for h, u, s in itertools.chain((
3705 (h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.items()), (
3706 (h, u, '.gitcookies')
3707 for h, (u, _) in a.gitcookies.items()))
3708 if h.endswith(_GOOGLESOURCE)]
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003709
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003710 if include_netrc:
3711 return self._all_hosts
3712 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003713
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003714 def print_current_creds(self, include_netrc=False):
3715 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3716 if not hosts:
3717 print('No Git/Gerrit credentials found')
3718 return
3719 lengths = [max(map(len, (row[i] for row in hosts))) for i in range(3)]
3720 header = [('Host', 'User', 'Which file'), ['=' * l for l in lengths]]
3721 for row in (header + hosts):
3722 print('\t'.join((('%%+%ds' % l) % s) for l, s in zip(lengths, row)))
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003723
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003724 @staticmethod
3725 def _parse_identity(identity):
3726 """Parses identity "git-<username>.domain" into <username> and domain."""
3727 # Special case: usernames that contain ".", which are generally not
3728 # distinguishable from sub-domains. But we do know typical domains:
3729 if identity.endswith('.chromium.org'):
3730 domain = 'chromium.org'
3731 username = identity[:-len('.chromium.org')]
3732 else:
3733 username, domain = identity.split('.', 1)
3734 if username.startswith('git-'):
3735 username = username[len('git-'):]
3736 return username, domain
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003737
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003738 def has_generic_host(self):
3739 """Returns whether generic .googlesource.com has been configured.
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003740
3741 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3742 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003743 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3744 if host == '.' + _GOOGLESOURCE:
3745 return True
3746 return False
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003747
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003748 def _get_git_gerrit_identity_pairs(self):
3749 """Returns map from canonic host to pair of identities (Git, Gerrit).
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003750
3751 One of identities might be None, meaning not configured.
3752 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003753 host_to_identity_pairs = {}
3754 for host, identity, _ in self.get_hosts_with_creds():
3755 canonical = _canonical_git_googlesource_host(host)
3756 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3757 idx = 0 if canonical == host else 1
3758 pair[idx] = identity
3759 return host_to_identity_pairs
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003760
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003761 def get_partially_configured_hosts(self):
3762 return set(
3763 (host if i1 else _canonical_gerrit_googlesource_host(host))
3764 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().items()
3765 if None in (i1, i2) and host != '.' + _GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003766
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003767 def get_conflicting_hosts(self):
3768 return set(
3769 host
3770 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().items()
3771 if None not in (i1, i2) and i1 != i2)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003772
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003773 def get_duplicated_hosts(self):
3774 counters = collections.Counter(
3775 h for h, _, _ in self.get_hosts_with_creds())
3776 return set(host for host, count in counters.items() if count > 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003777
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003778 @staticmethod
3779 def _format_hosts(hosts, extra_column_func=None):
3780 hosts = sorted(hosts)
3781 assert hosts
3782 if extra_column_func is None:
3783 extras = [''] * len(hosts)
3784 else:
3785 extras = [extra_column_func(host) for host in hosts]
3786 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len,
3787 extras)))
3788 lines = []
3789 for he in zip(hosts, extras):
3790 lines.append(tmpl % he)
3791 return lines
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003792
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003793 def _find_problems(self):
3794 if self.has_generic_host():
3795 yield ('.googlesource.com wildcard record detected', [
3796 'Chrome Infrastructure team recommends to list full host names '
3797 'explicitly.'
3798 ], None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003799
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003800 dups = self.get_duplicated_hosts()
3801 if dups:
3802 yield ('The following hosts were defined twice',
3803 self._format_hosts(dups), None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003804
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003805 partial = self.get_partially_configured_hosts()
3806 if partial:
3807 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3808 'These hosts are missing',
3809 self._format_hosts(
3810 partial, lambda host: 'but %s defined' %
3811 _get_counterpart_host(host)), partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003812
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003813 conflicting = self.get_conflicting_hosts()
3814 if conflicting:
3815 yield (
3816 'The following Git hosts have differing credentials from their '
3817 'Gerrit counterparts',
3818 self._format_hosts(
3819 conflicting, lambda host: '%s vs %s' % tuple(
3820 self._get_git_gerrit_identity_pairs()[host])),
3821 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003822
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003823 def find_and_report_problems(self):
3824 """Returns True if there was at least one problem, else False."""
3825 found = False
3826 bad_hosts = set()
3827 for title, sublines, hosts in self._find_problems():
3828 if not found:
3829 found = True
3830 print('\n\n.gitcookies problem report:\n')
3831 bad_hosts.update(hosts or [])
3832 print(' %s%s' % (title, (':' if sublines else '')))
3833 if sublines:
3834 print()
3835 print(' %s' % '\n '.join(sublines))
3836 print()
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003837
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003838 if bad_hosts:
3839 assert found
3840 print(
3841 ' You can manually remove corresponding lines in your %s file and '
3842 'visit the following URLs with correct account to generate '
3843 'correct credential lines:\n' %
3844 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
3845 print(' %s' % '\n '.join(
3846 sorted(
3847 set(gerrit_util.CookiesAuthenticator().get_new_password_url(
3848 _canonical_git_googlesource_host(host))
3849 for host in bad_hosts))))
3850 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003851
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003852
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003853@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003854def CMDcreds_check(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003855 """Checks credentials and suggests changes."""
3856 _, _ = parser.parse_args(args)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003857
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003858 # Code below checks .gitcookies. Abort if using something else.
3859 authn = gerrit_util.Authenticator.get()
3860 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
3861 message = (
3862 'This command is not designed for bot environment. It checks '
3863 '~/.gitcookies file not generally used on bots.')
3864 # TODO(crbug.com/1059384): Automatically detect when running on
3865 # cloudtop.
3866 if isinstance(authn, gerrit_util.GceAuthenticator):
3867 message += (
3868 '\n'
3869 'If you need to run this on GCE or a cloudtop instance, '
3870 'export SKIP_GCE_AUTH_FOR_GIT=1 in your env.')
3871 DieWithError(message)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003872
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003873 checker = _GitCookiesChecker()
3874 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003875
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003876 print('Your .netrc and .gitcookies have credentials for these hosts:')
3877 checker.print_current_creds(include_netrc=True)
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003878
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003879 if not checker.find_and_report_problems():
3880 print('\nNo problems detected in your .gitcookies file.')
3881 return 0
3882 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003883
3884
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003885@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003886def CMDbaseurl(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003887 """Gets or sets base-url for this branch."""
3888 _, args = parser.parse_args(args)
3889 branchref = scm.GIT.GetBranchRef(settings.GetRoot())
3890 branch = scm.GIT.ShortBranchName(branchref)
3891 if not args:
3892 print('Current base-url:')
3893 return RunGit(['config', 'branch.%s.base-url' % branch],
3894 error_ok=False).strip()
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00003895
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003896 print('Setting base-url to %s' % args[0])
3897 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3898 error_ok=False).strip()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003899
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003900
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003901def color_for_status(status):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003902 """Maps a Changelist status to color, for CMDstatus and other tools."""
3903 BOLD = '\033[1m'
3904 return {
3905 'unsent': BOLD + Fore.YELLOW,
3906 'waiting': BOLD + Fore.RED,
3907 'reply': BOLD + Fore.YELLOW,
3908 'not lgtm': BOLD + Fore.RED,
3909 'lgtm': BOLD + Fore.GREEN,
3910 'commit': BOLD + Fore.MAGENTA,
3911 'closed': BOLD + Fore.CYAN,
3912 'error': BOLD + Fore.WHITE,
3913 }.get(status, Fore.WHITE)
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003914
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003915
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003916def get_cl_statuses(changes, fine_grained, max_processes=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003917 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003918
3919 If fine_grained is true, this will fetch CL statuses from the server.
3920 Otherwise, simply indicate if there's a matching url for the given branches.
3921
3922 If max_processes is specified, it is used as the maximum number of processes
3923 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3924 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003925
3926 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003927 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003928 if not changes:
3929 return
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003930
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003931 if not fine_grained:
3932 # Fast path which doesn't involve querying codereview servers.
3933 # Do not use get_approving_reviewers(), since it requires an HTTP
3934 # request.
3935 for cl in changes:
3936 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
3937 return
3938
3939 # First, sort out authentication issues.
3940 logging.debug('ensuring credentials exist')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003941 for cl in changes:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003942 cl.EnsureAuthenticated(force=False, refresh=True)
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003943
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003944 def fetch(cl):
3945 try:
3946 return (cl, cl.GetStatus())
3947 except:
3948 # See http://crbug.com/629863.
3949 logging.exception('failed to fetch status for cl %s:',
3950 cl.GetIssue())
3951 raise
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003952
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003953 threads_count = len(changes)
3954 if max_processes:
3955 threads_count = max(1, min(threads_count, max_processes))
3956 logging.debug('querying %d CLs using %d threads', len(changes),
3957 threads_count)
3958
3959 pool = multiprocessing.pool.ThreadPool(threads_count)
3960 fetched_cls = set()
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003961 try:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003962 it = pool.imap_unordered(fetch, changes).__iter__()
3963 while True:
3964 try:
3965 cl, status = it.next(timeout=5)
3966 except (multiprocessing.TimeoutError, StopIteration):
3967 break
3968 fetched_cls.add(cl)
3969 yield cl, status
3970 finally:
3971 pool.close()
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003972
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003973 # Add any branches that failed to fetch.
3974 for cl in set(changes) - fetched_cls:
3975 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003976
rmistry@google.com2dd99862015-06-22 12:22:18 +00003977
Jose Lopes3863fc52020-04-07 17:00:25 +00003978def upload_branch_deps(cl, args, force=False):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003979 """Uploads CLs of local branches that are dependents of the current branch.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003980
3981 If the local branch dependency tree looks like:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003982
3983 test1 -> test2.1 -> test3.1
3984 -> test3.2
3985 -> test2.2 -> test3.3
rmistry@google.com2dd99862015-06-22 12:22:18 +00003986
3987 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3988 run on the dependent branches in this order:
3989 test2.1, test3.1, test3.2, test2.2, test3.3
3990
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003991 Note: This function does not rebase your local dependent branches. Use it
3992 when you make a change to the parent branch that will not conflict
3993 with its dependent branches, and you would like their dependencies
3994 updated in Rietveld.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003995 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003996 if git_common.is_dirty_git_tree('upload-branch-deps'):
3997 return 1
rmistry@google.com2dd99862015-06-22 12:22:18 +00003998
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003999 root_branch = cl.GetBranch()
4000 if root_branch is None:
4001 DieWithError('Can\'t find dependent branches from detached HEAD state. '
4002 'Get on a branch!')
4003 if not cl.GetIssue():
4004 DieWithError(
4005 'Current branch does not have an uploaded CL. We cannot set '
4006 'patchset dependencies without an uploaded CL.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004007
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004008 branches = RunGit([
4009 'for-each-ref', '--format=%(refname:short) %(upstream:short)',
4010 'refs/heads'
4011 ])
4012 if not branches:
4013 print('No local branches found.')
4014 return 0
rmistry@google.com2dd99862015-06-22 12:22:18 +00004015
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004016 # Create a dictionary of all local branches to the branches that are
4017 # dependent on it.
4018 tracked_to_dependents = collections.defaultdict(list)
4019 for b in branches.splitlines():
4020 tokens = b.split()
4021 if len(tokens) == 2:
4022 branch_name, tracked = tokens
4023 tracked_to_dependents[tracked].append(branch_name)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004024
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004025 print()
4026 print('The dependent local branches of %s are:' % root_branch)
4027 dependents = []
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004028
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004029 def traverse_dependents_preorder(branch, padding=''):
4030 dependents_to_process = tracked_to_dependents.get(branch, [])
4031 padding += ' '
4032 for dependent in dependents_to_process:
4033 print('%s%s' % (padding, dependent))
4034 dependents.append(dependent)
4035 traverse_dependents_preorder(dependent, padding)
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004036
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004037 traverse_dependents_preorder(root_branch)
4038 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004039
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004040 if not dependents:
4041 print('There are no dependent local branches for %s' % root_branch)
4042 return 0
rmistry@google.com2dd99862015-06-22 12:22:18 +00004043
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004044 # Record all dependents that failed to upload.
4045 failures = {}
4046 # Go through all dependents, checkout the branch and upload.
4047 try:
4048 for dependent_branch in dependents:
4049 print()
4050 print('--------------------------------------')
4051 print('Running "git cl upload" from %s:' % dependent_branch)
4052 RunGit(['checkout', '-q', dependent_branch])
4053 print()
4054 try:
4055 if CMDupload(OptionParser(), args) != 0:
4056 print('Upload failed for %s!' % dependent_branch)
4057 failures[dependent_branch] = 1
4058 except: # pylint: disable=bare-except
4059 failures[dependent_branch] = 1
4060 print()
4061 finally:
4062 # Swap back to the original root branch.
4063 RunGit(['checkout', '-q', root_branch])
4064
4065 print()
4066 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004067 for dependent_branch in dependents:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004068 upload_status = 'failed' if failures.get(
4069 dependent_branch) else 'succeeded'
4070 print(' %s : %s' % (dependent_branch, upload_status))
4071 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004072
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004073 return 0
rmistry@google.com2dd99862015-06-22 12:22:18 +00004074
4075
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00004076def GetArchiveTagForBranch(issue_num, branch_name, existing_tags, pattern):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004077 """Given a proposed tag name, returns a tag name that is guaranteed to be
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00004078 unique. If 'foo' is proposed but already exists, then 'foo-2' is used,
4079 or 'foo-3', and so on."""
4080
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004081 proposed_tag = pattern.format(**{'issue': issue_num, 'branch': branch_name})
4082 for suffix_num in itertools.count(1):
4083 if suffix_num == 1:
4084 to_check = proposed_tag
4085 else:
4086 to_check = '%s-%d' % (proposed_tag, suffix_num)
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00004087
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004088 if to_check not in existing_tags:
4089 return to_check
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00004090
4091
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004092@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07004093def CMDarchive(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004094 """Archives and deletes branches associated with closed changelists."""
4095 parser.add_option(
4096 '-j',
4097 '--maxjobs',
4098 action='store',
4099 type=int,
4100 help='The maximum number of jobs to use when retrieving review status.')
4101 parser.add_option('-f',
4102 '--force',
4103 action='store_true',
4104 help='Bypasses the confirmation prompt.')
4105 parser.add_option('-d',
4106 '--dry-run',
4107 action='store_true',
4108 help='Skip the branch tagging and removal steps.')
4109 parser.add_option('-t',
4110 '--notags',
4111 action='store_true',
4112 help='Do not tag archived branches. '
4113 'Note: local commit history may be lost.')
4114 parser.add_option('-p',
4115 '--pattern',
4116 default='git-cl-archived-{issue}-{branch}',
4117 help='Format string for archive tags. '
4118 'E.g. \'archived-{issue}-{branch}\'.')
kmarshall3bff56b2016-06-06 18:31:47 -07004119
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004120 options, args = parser.parse_args(args)
4121 if args:
4122 parser.error('Unsupported args: %s' % ' '.join(args))
kmarshall3bff56b2016-06-06 18:31:47 -07004123
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004124 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4125 if not branches:
4126 return 0
4127
4128 tags = RunGit(['for-each-ref', '--format=%(refname)', 'refs/tags'
4129 ]).splitlines() or []
4130 tags = [t.split('/')[-1] for t in tags]
4131
4132 print('Finding all branches associated with closed issues...')
4133 changes = [Changelist(branchref=b) for b in branches.splitlines()]
4134 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4135 statuses = get_cl_statuses(changes,
4136 fine_grained=True,
4137 max_processes=options.maxjobs)
4138 proposal = [(cl.GetBranch(),
4139 GetArchiveTagForBranch(cl.GetIssue(), cl.GetBranch(), tags,
4140 options.pattern))
4141 for cl, status in statuses
4142 if status in ('closed', 'rietveld-not-supported')]
4143 proposal.sort()
4144
4145 if not proposal:
4146 print('No branches with closed codereview issues found.')
4147 return 0
4148
4149 current_branch = scm.GIT.GetBranch(settings.GetRoot())
4150
4151 print('\nBranches with closed issues that will be archived:\n')
4152 if options.notags:
4153 for next_item in proposal:
4154 print(' ' + next_item[0])
4155 else:
4156 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4157 for next_item in proposal:
4158 print('%*s %s' % (alignment, next_item[0], next_item[1]))
4159
4160 # Quit now on precondition failure or if instructed by the user, either
4161 # via an interactive prompt or by command line flags.
4162 if options.dry_run:
4163 print('\nNo changes were made (dry run).\n')
4164 return 0
4165
4166 if any(branch == current_branch for branch, _ in proposal):
4167 print('You are currently on a branch \'%s\' which is associated with a '
4168 'closed codereview issue, so archive cannot proceed. Please '
4169 'checkout another branch and run this command again.' %
4170 current_branch)
4171 return 1
4172
4173 if not options.force:
4174 answer = gclient_utils.AskForData(
4175 '\nProceed with deletion (Y/n)? ').lower()
4176 if answer not in ('y', ''):
4177 print('Aborted.')
4178 return 1
4179
4180 for branch, tagname in proposal:
4181 if not options.notags:
4182 RunGit(['tag', tagname, branch])
4183
4184 if RunGitWithCode(['branch', '-D', branch])[0] != 0:
4185 # Clean up the tag if we failed to delete the branch.
4186 RunGit(['tag', '-d', tagname])
4187
4188 print('\nJob\'s done!')
4189
kmarshall3bff56b2016-06-06 18:31:47 -07004190 return 0
4191
kmarshall3bff56b2016-06-06 18:31:47 -07004192
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004193@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004194def CMDstatus(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004195 """Show status of changelists.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004196
4197 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004198 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004199 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004200 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004201 - Red 'not LGTM'ed
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004202 - Magenta in the CQ
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004203 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004204 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004205
4206 Also see 'git cl comments'.
4207 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004208 parser.add_option('--no-branch-color',
4209 action='store_true',
4210 help='Disable colorized branch names')
4211 parser.add_option(
4212 '--field', help='print only specific field (desc|id|patch|status|url)')
4213 parser.add_option('-f',
4214 '--fast',
4215 action='store_true',
4216 help='Do not retrieve review status')
4217 parser.add_option(
4218 '-j',
4219 '--maxjobs',
4220 action='store',
4221 type=int,
4222 help='The maximum number of jobs to use when retrieving review status')
4223 parser.add_option(
4224 '-i',
4225 '--issue',
4226 type=int,
4227 help='Operate on this issue instead of the current branch\'s implicit '
4228 'issue. Requires --field to be set.')
4229 parser.add_option('-d',
4230 '--date-order',
4231 action='store_true',
4232 help='Order branches by committer date.')
4233 options, args = parser.parse_args(args)
4234 if args:
4235 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004236
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004237 if options.issue is not None and not options.field:
4238 parser.error('--field must be given when --issue is set.')
iannucci3c972b92016-08-17 13:24:10 -07004239
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004240 if options.field:
4241 cl = Changelist(issue=options.issue)
4242 if options.field.startswith('desc'):
4243 if cl.GetIssue():
4244 print(cl.FetchDescription())
4245 elif options.field == 'id':
4246 issueid = cl.GetIssue()
4247 if issueid:
4248 print(issueid)
4249 elif options.field == 'patch':
4250 patchset = cl.GetMostRecentPatchset()
4251 if patchset:
4252 print(patchset)
4253 elif options.field == 'status':
4254 print(cl.GetStatus())
4255 elif options.field == 'url':
4256 url = cl.GetIssueURL()
4257 if url:
4258 print(url)
4259 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004260
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004261 branches = RunGit([
4262 'for-each-ref', '--format=%(refname) %(committerdate:unix)',
4263 'refs/heads'
4264 ])
4265 if not branches:
4266 print('No local branch found.')
4267 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004268
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004269 changes = [
4270 Changelist(branchref=b, commit_date=ct)
4271 for b, ct in map(lambda line: line.split(' '), branches.splitlines())
4272 ]
4273 print('Branches associated with reviews:')
4274 output = get_cl_statuses(changes,
4275 fine_grained=not options.fast,
4276 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004277
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004278 current_branch = scm.GIT.GetBranch(settings.GetRoot())
Daniel McArdlea23bf592019-02-12 00:25:12 +00004279
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004280 def FormatBranchName(branch, colorize=False):
4281 """Simulates 'git branch' behavior. Colorizes and prefixes branch name with
Daniel McArdlea23bf592019-02-12 00:25:12 +00004282 an asterisk when it is the current branch."""
4283
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004284 asterisk = ""
4285 color = Fore.RESET
4286 if branch == current_branch:
4287 asterisk = "* "
4288 color = Fore.GREEN
4289 branch_name = scm.GIT.ShortBranchName(branch)
Daniel McArdlea23bf592019-02-12 00:25:12 +00004290
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004291 if colorize:
4292 return asterisk + color + branch_name + Fore.RESET
4293 return asterisk + branch_name
Daniel McArdle452a49f2019-02-14 17:28:31 +00004294
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004295 branch_statuses = {}
Daniel McArdlea23bf592019-02-12 00:25:12 +00004296
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004297 alignment = max(5,
4298 max(len(FormatBranchName(c.GetBranch())) for c in changes))
Sigurd Schneider1bfda8e2021-06-30 14:46:25 +00004299
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004300 if options.date_order or settings.IsStatusCommitOrderByDate():
4301 sorted_changes = sorted(changes,
4302 key=lambda c: c.GetCommitDate(),
4303 reverse=True)
4304 else:
4305 sorted_changes = sorted(changes, key=lambda c: c.GetBranch())
4306 for cl in sorted_changes:
4307 branch = cl.GetBranch()
4308 while branch not in branch_statuses:
4309 c, status = next(output)
4310 branch_statuses[c.GetBranch()] = status
4311 status = branch_statuses.pop(branch)
4312 url = cl.GetIssueURL(short=True)
4313 if url and (not status or status == 'error'):
4314 # The issue probably doesn't exist anymore.
4315 url += ' (broken)'
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004316
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004317 color = color_for_status(status)
4318 # Turn off bold as well as colors.
4319 END = '\033[0m'
4320 reset = Fore.RESET + END
4321 if not setup_color.IS_TTY:
4322 color = ''
4323 reset = ''
4324 status_str = '(%s)' % status if status else ''
Daniel McArdle452a49f2019-02-14 17:28:31 +00004325
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004326 branch_display = FormatBranchName(branch)
4327 padding = ' ' * (alignment - len(branch_display))
4328 if not options.no_branch_color:
4329 branch_display = FormatBranchName(branch, colorize=True)
Daniel McArdle452a49f2019-02-14 17:28:31 +00004330
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004331 print(' %s : %s%s %s%s' %
4332 (padding + branch_display, color, url, status_str, reset))
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004333
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004334 print()
4335 print('Current branch: %s' % current_branch)
4336 for cl in changes:
4337 if cl.GetBranch() == current_branch:
4338 break
4339 if not cl.GetIssue():
4340 print('No issue assigned.')
4341 return 0
4342 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4343 if not options.fast:
4344 print('Issue description:')
4345 print(cl.FetchDescription(pretty=True))
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004346 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004347
4348
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004349def colorize_CMDstatus_doc():
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004350 """To be called once in main() to add colors to git cl status help."""
4351 colors = [i for i in dir(Fore) if i[0].isupper()]
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004352
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004353 def colorize_line(line):
4354 for color in colors:
4355 if color in line.upper():
4356 # Extract whitespace first and the leading '-'.
4357 indent = len(line) - len(line.lstrip(' ')) + 1
4358 return line[:indent] + getattr(
4359 Fore, color) + line[indent:] + Fore.RESET
4360 return line
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004361
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004362 lines = CMDstatus.__doc__.splitlines()
4363 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004364
4365
phajdan.jre328cf92016-08-22 04:12:17 -07004366def write_json(path, contents):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004367 if path == '-':
4368 json.dump(contents, sys.stdout)
4369 else:
4370 with open(path, 'w') as f:
4371 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004372
4373
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004374@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004375@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004376def CMDissue(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004377 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004378
4379 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004380 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004381 parser.add_option('-r',
4382 '--reverse',
4383 action='store_true',
4384 help='Lookup the branch(es) for the specified issues. If '
4385 'no issues are specified, all branches with mapped '
4386 'issues will be listed.')
4387 parser.add_option('--json',
4388 help='Path to JSON output file, or "-" for stdout.')
4389 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004390
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004391 if options.reverse:
4392 branches = RunGit(['for-each-ref', 'refs/heads',
4393 '--format=%(refname)']).splitlines()
4394 # Reverse issue lookup.
4395 issue_branch_map = {}
Arthur Milchior801a9752023-04-07 10:33:54 +00004396
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004397 git_config = {}
4398 for config in RunGit(['config', '--get-regexp',
4399 r'branch\..*issue']).splitlines():
4400 name, _space, val = config.partition(' ')
4401 git_config[name] = val
Arthur Milchior801a9752023-04-07 10:33:54 +00004402
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004403 for branch in branches:
4404 issue = git_config.get(
4405 'branch.%s.%s' %
4406 (scm.GIT.ShortBranchName(branch), ISSUE_CONFIG_KEY))
4407 if issue:
4408 issue_branch_map.setdefault(int(issue), []).append(branch)
4409 if not args:
4410 args = sorted(issue_branch_map.keys())
4411 result = {}
4412 for issue in args:
4413 try:
4414 issue_num = int(issue)
4415 except ValueError:
4416 print('ERROR cannot parse issue number: %s' % issue,
4417 file=sys.stderr)
4418 continue
4419 result[issue_num] = issue_branch_map.get(issue_num)
4420 print('Branch for issue number %s: %s' % (issue, ', '.join(
4421 issue_branch_map.get(issue_num) or ('None', ))))
4422 if options.json:
4423 write_json(options.json, result)
4424 return 0
4425
4426 if len(args) > 0:
4427 issue = ParseIssueNumberArgument(args[0])
4428 if not issue.valid:
4429 DieWithError(
4430 'Pass a url or number to set the issue, 0 to unset it, '
4431 'or no argument to list it.\n'
4432 'Maybe you want to run git cl status?')
4433 cl = Changelist()
4434 cl.SetIssue(issue.issue)
4435 else:
4436 cl = Changelist()
4437 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
Arthur Milchior801a9752023-04-07 10:33:54 +00004438 if options.json:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004439 write_json(
4440 options.json, {
4441 'gerrit_host': cl.GetGerritHost(),
4442 'gerrit_project': cl.GetGerritProject(),
4443 'issue_url': cl.GetIssueURL(),
4444 'issue': cl.GetIssue(),
4445 })
Arthur Milchior801a9752023-04-07 10:33:54 +00004446 return 0
Aaron Gable78753da2017-06-15 10:35:49 -07004447
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004448
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004449@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004450def CMDcomments(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004451 """Shows or posts review comments for any changelist."""
4452 parser.add_option('-a',
4453 '--add-comment',
4454 dest='comment',
4455 help='comment to add to an issue')
4456 parser.add_option('-p',
4457 '--publish',
4458 action='store_true',
4459 help='marks CL as ready and sends comment to reviewers')
4460 parser.add_option('-i',
4461 '--issue',
4462 dest='issue',
4463 help='review issue id (defaults to current issue).')
4464 parser.add_option('-m',
4465 '--machine-readable',
4466 dest='readable',
4467 action='store_false',
4468 default=True,
4469 help='output comments in a format compatible with '
4470 'editor parsing')
4471 parser.add_option('-j',
4472 '--json-file',
4473 help='File to write JSON summary to, or "-" for stdout')
4474 options, args = parser.parse_args(args)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004475
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004476 issue = None
4477 if options.issue:
4478 try:
4479 issue = int(options.issue)
4480 except ValueError:
4481 DieWithError('A review issue ID is expected to be a number.')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004482
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004483 cl = Changelist(issue=issue)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004484
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004485 if options.comment:
4486 cl.AddComment(options.comment, options.publish)
4487 return 0
4488
4489 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4490 key=lambda c: c.date)
4491 for comment in summary:
4492 if comment.disapproval:
4493 color = Fore.RED
4494 elif comment.approval:
4495 color = Fore.GREEN
4496 elif comment.sender == cl.GetIssueOwner():
4497 color = Fore.MAGENTA
4498 elif comment.autogenerated:
4499 color = Fore.CYAN
4500 else:
4501 color = Fore.BLUE
4502 print('\n%s%s %s%s\n%s' %
4503 (color, comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4504 comment.sender, Fore.RESET, '\n'.join(
4505 ' ' + l for l in comment.message.strip().splitlines())))
4506
4507 if options.json_file:
4508
4509 def pre_serialize(c):
4510 dct = c._asdict().copy()
4511 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4512 return dct
4513
4514 write_json(options.json_file, [pre_serialize(x) for x in summary])
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004515 return 0
4516
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004517
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004518@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004519@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004520def CMDdescription(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004521 """Brings up the editor for the current CL's description."""
4522 parser.add_option(
4523 '-d',
4524 '--display',
4525 action='store_true',
4526 help='Display the description instead of opening an editor')
4527 parser.add_option(
4528 '-n',
4529 '--new-description',
4530 help='New description to set for this issue (- for stdin, '
4531 '+ to load from local commit HEAD)')
4532 parser.add_option('-f',
4533 '--force',
4534 action='store_true',
4535 help='Delete any unpublished Gerrit edits for this issue '
4536 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004537
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004538 options, args = parser.parse_args(args)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004539
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004540 target_issue_arg = None
4541 if len(args) > 0:
4542 target_issue_arg = ParseIssueNumberArgument(args[0])
4543 if not target_issue_arg.valid:
4544 parser.error('Invalid issue ID or URL.')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004545
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004546 kwargs = {}
4547 if target_issue_arg:
4548 kwargs['issue'] = target_issue_arg.issue
4549 kwargs['codereview_host'] = target_issue_arg.hostname
martiniss6eda05f2016-06-30 10:18:35 -07004550
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004551 cl = Changelist(**kwargs)
4552 if not cl.GetIssue():
4553 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004554
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004555 if args and not args[0].isdigit():
4556 logging.info('canonical issue/change URL: %s\n', cl.GetIssueURL())
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004557
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004558 description = ChangeDescription(cl.FetchDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004559
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004560 if options.display:
4561 print(description.description)
4562 return 0
4563
4564 if options.new_description:
4565 text = options.new_description
4566 if text == '-':
4567 text = '\n'.join(l.rstrip() for l in sys.stdin)
4568 elif text == '+':
4569 base_branch = cl.GetCommonAncestorWithUpstream()
4570 text = _create_description_from_log([base_branch])
4571
4572 description.set_description(text)
4573 else:
4574 description.prompt()
4575 if cl.FetchDescription().strip() != description.description:
4576 cl.UpdateDescription(description.description, force=options.force)
smut@google.com34fb6b12015-07-13 20:03:26 +00004577 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004578
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004579
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004580@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004581def CMDlint(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004582 """Runs cpplint on the current changelist."""
4583 parser.add_option(
4584 '--filter',
4585 action='append',
4586 metavar='-x,+y',
4587 help='Comma-separated list of cpplint\'s category-filters')
4588 options, args = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004589
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004590 # Access to a protected member _XX of a client class
4591 # pylint: disable=protected-access
4592 try:
4593 import cpplint
4594 import cpplint_chromium
4595 except ImportError:
4596 print(
4597 'Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.'
4598 )
4599 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004600
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004601 # Change the current working directory before calling lint so that it
4602 # shows the correct base.
4603 previous_cwd = os.getcwd()
4604 os.chdir(settings.GetRoot())
4605 try:
4606 cl = Changelist()
4607 files = cl.GetAffectedFiles(cl.GetCommonAncestorWithUpstream())
4608 if not files:
4609 print('Cannot lint an empty CL')
4610 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004611
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004612 # Process cpplint arguments, if any.
4613 filters = presubmit_canned_checks.GetCppLintFilters(options.filter)
4614 command = ['--filter=' + ','.join(filters)]
4615 command.extend(args)
4616 command.extend(files)
4617 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004618
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004619 include_regex = re.compile(settings.GetLintRegex())
4620 ignore_regex = re.compile(settings.GetLintIgnoreRegex())
4621 extra_check_functions = [
4622 cpplint_chromium.CheckPointerDeclarationWhitespace
4623 ]
4624 for filename in filenames:
4625 if not include_regex.match(filename):
4626 print('Skipping file %s' % filename)
4627 continue
Lei Zhang379d1ad2020-07-15 19:40:06 +00004628
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004629 if ignore_regex.match(filename):
4630 print('Ignoring file %s' % filename)
4631 continue
Lei Zhang379d1ad2020-07-15 19:40:06 +00004632
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004633 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4634 extra_check_functions)
4635 finally:
4636 os.chdir(previous_cwd)
4637 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
4638 if cpplint._cpplint_state.error_count != 0:
4639 return 1
4640 return 0
thestig@chromium.org44202a22014-03-11 19:22:18 +00004641
4642
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004643@metrics.collector.collect_metrics('git cl presubmit')
mlcuic601e362023-08-14 23:39:46 +00004644@subcommand.usage('[base branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004645def CMDpresubmit(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004646 """Runs presubmit tests on the current changelist."""
4647 parser.add_option('-u',
4648 '--upload',
4649 action='store_true',
4650 help='Run upload hook instead of the push hook')
4651 parser.add_option('-f',
4652 '--force',
4653 action='store_true',
4654 help='Run checks even if tree is dirty')
4655 parser.add_option(
4656 '--all',
4657 action='store_true',
4658 help='Run checks against all files, not just modified ones')
4659 parser.add_option('--files',
4660 nargs=1,
4661 help='Semicolon-separated list of files to be marked as '
4662 'modified when executing presubmit or post-upload hooks. '
4663 'fnmatch wildcards can also be used.')
4664 parser.add_option(
4665 '--parallel',
4666 action='store_true',
4667 help='Run all tests specified by input_api.RunTests in all '
4668 'PRESUBMIT files in parallel.')
4669 parser.add_option('--resultdb',
4670 action='store_true',
4671 help='Run presubmit checks in the ResultSink environment '
4672 'and send results to the ResultDB database.')
4673 parser.add_option('--realm', help='LUCI realm if reporting to ResultDB')
4674 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004675
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004676 if not options.force and git_common.is_dirty_git_tree('presubmit'):
4677 print('use --force to check even if tree is dirty.')
4678 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004679
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004680 cl = Changelist()
4681 if args:
4682 base_branch = args[0]
Bruce Dawsoneb8426e2022-08-05 23:58:15 +00004683 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004684 # Default to diffing against the common ancestor of the upstream branch.
4685 base_branch = cl.GetCommonAncestorWithUpstream()
Aaron Gable8076c282017-11-29 14:39:41 -08004686
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004687 start = time.time()
4688 try:
4689 if not 'PRESUBMIT_SKIP_NETWORK' in os.environ and cl.GetIssue():
4690 description = cl.FetchDescription()
4691 else:
4692 description = _create_description_from_log([base_branch])
4693 except Exception as e:
4694 print('Failed to fetch CL description - %s' % str(e))
4695 description = _create_description_from_log([base_branch])
4696 elapsed = time.time() - start
4697 if elapsed > 5:
4698 print('%.1f s to get CL description.' % elapsed)
Bruce Dawson13acea32022-05-03 22:13:08 +00004699
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004700 if not base_branch:
4701 if not options.force:
4702 print('use --force to check even when not on a branch.')
4703 return 1
4704 base_branch = 'HEAD'
4705
4706 cl.RunHook(committing=not options.upload,
4707 may_prompt=False,
4708 verbose=options.verbose,
4709 parallel=options.parallel,
4710 upstream=base_branch,
4711 description=description,
4712 all_files=options.all,
4713 files=options.files,
4714 resultdb=options.resultdb,
4715 realm=options.realm)
4716 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004717
4718
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004719def GenerateGerritChangeId(message):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004720 """Returns the Change ID footer value (Ixxxxxx...xxx).
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004721
4722 Works the same way as
4723 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4724 but can be called on demand on all platforms.
4725
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004726 The basic idea is to generate git hash of a state of the tree, original
4727 commit message, author/committer info and timestamps.
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004728 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004729 lines = []
4730 tree_hash = RunGitSilent(['write-tree'])
4731 lines.append('tree %s' % tree_hash.strip())
4732 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'],
4733 suppress_stderr=False)
4734 if code == 0:
4735 lines.append('parent %s' % parent.strip())
4736 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4737 lines.append('author %s' % author.strip())
4738 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4739 lines.append('committer %s' % committer.strip())
4740 lines.append('')
4741 # Note: Gerrit's commit-hook actually cleans message of some lines and
4742 # whitespace. This code is not doing this, but it clearly won't decrease
4743 # entropy.
4744 lines.append(message)
4745 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4746 stdin=('\n'.join(lines)).encode())
4747 return 'I%s' % change_hash.strip()
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004748
4749
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004750def GetTargetRef(remote, remote_branch, target_branch):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004751 """Computes the remote branch ref to use for the CL.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004752
4753 Args:
4754 remote (str): The git remote for the CL.
4755 remote_branch (str): The git remote branch for the CL.
4756 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004757 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004758 if not (remote and remote_branch):
4759 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004760
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004761 if target_branch:
4762 # Canonicalize branch references to the equivalent local full symbolic
4763 # refs, which are then translated into the remote full symbolic refs
4764 # below.
4765 if '/' not in target_branch:
4766 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4767 else:
4768 prefix_replacements = (
4769 ('^((refs/)?remotes/)?branch-heads/',
4770 'refs/remotes/branch-heads/'),
4771 ('^((refs/)?remotes/)?%s/' % remote,
4772 'refs/remotes/%s/' % remote),
4773 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4774 )
4775 match = None
4776 for regex, replacement in prefix_replacements:
4777 match = re.search(regex, target_branch)
4778 if match:
4779 remote_branch = target_branch.replace(
4780 match.group(0), replacement)
4781 break
4782 if not match:
4783 # This is a branch path but not one we recognize; use as-is.
4784 remote_branch = target_branch
4785 # pylint: disable=consider-using-get
4786 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4787 # pylint: enable=consider-using-get
4788 # Handle the refs that need to land in different refs.
4789 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004790
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004791 # Create the true path to the remote branch.
4792 # Does the following translation:
4793 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4794 # * refs/remotes/origin/main -> refs/heads/main
4795 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4796 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4797 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4798 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4799 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4800 'refs/heads/')
4801 elif remote_branch.startswith('refs/remotes/branch-heads'):
4802 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004803
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004804 return remote_branch
wittman@chromium.org455dc922015-01-26 20:15:50 +00004805
4806
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004807def cleanup_list(l):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004808 """Fixes a list so that comma separated items are put as individual items.
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004809
4810 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4811 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4812 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004813 items = sum((i.split(',') for i in l), [])
4814 stripped_items = (i.strip() for i in items)
4815 return sorted(filter(None, stripped_items))
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004816
4817
Aaron Gable4db38df2017-11-03 14:59:07 -07004818@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004819@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004820def CMDupload(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004821 """Uploads the current changelist to codereview.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004822
4823 Can skip dependency patchset uploads for a branch by running:
4824 git config branch.branch_name.skip-deps-uploads True
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004825 To unset, run:
rmistry@google.com78948ed2015-07-08 23:09:57 +00004826 git config --unset branch.branch_name.skip-deps-uploads
4827 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004828
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004829 If the name of the checked out branch starts with "bug-" or "fix-" followed
4830 by a bug number, this bug number is automatically populated in the CL
Dominic Battre7d1c4842017-10-27 09:17:28 +02004831 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004832
4833 If subject contains text in square brackets or has "<text>: " prefix, such
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004834 text(s) is treated as Gerrit hashtags. For example, CLs with subjects:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004835 [git-cl] add support for hashtags
4836 Foo bar: implement foo
4837 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004838 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004839 parser.add_option('--bypass-hooks',
4840 action='store_true',
4841 dest='bypass_hooks',
4842 help='bypass upload presubmit hook')
4843 parser.add_option('--bypass-watchlists',
4844 action='store_true',
4845 dest='bypass_watchlists',
4846 help='bypass watchlists auto CC-ing reviewers')
4847 parser.add_option('-f',
4848 '--force',
4849 action='store_true',
4850 dest='force',
4851 help="force yes to questions (don't prompt)")
4852 parser.add_option('--message',
4853 '-m',
4854 dest='message',
4855 help='message for patchset')
4856 parser.add_option('-b',
4857 '--bug',
4858 help='pre-populate the bug number(s) for this issue. '
4859 'If several, separate with commas')
4860 parser.add_option('--message-file',
4861 dest='message_file',
4862 help='file which contains message for patchset')
4863 parser.add_option('--title', '-t', dest='title', help='title for patchset')
4864 parser.add_option('-T',
4865 '--skip-title',
4866 action='store_true',
4867 dest='skip_title',
4868 help='Use the most recent commit message as the title of '
4869 'the patchset')
4870 parser.add_option('-r',
4871 '--reviewers',
4872 action='append',
4873 default=[],
4874 help='reviewer email addresses')
4875 parser.add_option('--cc',
4876 action='append',
4877 default=[],
4878 help='cc email addresses')
4879 parser.add_option('--hashtag',
4880 dest='hashtags',
4881 action='append',
4882 default=[],
4883 help=('Gerrit hashtag for new CL; '
4884 'can be applied multiple times'))
4885 parser.add_option('-s',
4886 '--send-mail',
4887 '--send-email',
4888 dest='send_mail',
4889 action='store_true',
4890 help='send email to reviewer(s) and cc(s) immediately')
4891 parser.add_option('--target_branch',
4892 '--target-branch',
4893 metavar='TARGET',
4894 help='Apply CL to remote ref TARGET. ' +
4895 'Default: remote branch head, or main')
4896 parser.add_option('--squash',
4897 action='store_true',
4898 help='Squash multiple commits into one')
4899 parser.add_option('--no-squash',
4900 action='store_false',
4901 dest='squash',
4902 help='Don\'t squash multiple commits into one')
4903 parser.add_option('--topic',
4904 default=None,
4905 help='Topic to specify when uploading')
4906 parser.add_option('--r-owners',
4907 dest='add_owners_to',
4908 action='store_const',
4909 const='R',
4910 help='add a set of OWNERS to R')
4911 parser.add_option('-c',
4912 '--use-commit-queue',
4913 action='store_true',
4914 default=False,
4915 help='tell the CQ to commit this patchset; '
4916 'implies --send-mail')
4917 parser.add_option('-d',
4918 '--cq-dry-run',
4919 action='store_true',
4920 default=False,
4921 help='Send the patchset to do a CQ dry run right after '
4922 'upload.')
4923 parser.add_option('--set-bot-commit',
4924 action='store_true',
4925 help=optparse.SUPPRESS_HELP)
4926 parser.add_option('--preserve-tryjobs',
4927 action='store_true',
4928 help='instruct the CQ to let tryjobs running even after '
4929 'new patchsets are uploaded instead of canceling '
4930 'prior patchset\' tryjobs')
4931 parser.add_option(
4932 '--dependencies',
4933 action='store_true',
4934 help='Uploads CLs of all the local branches that depend on '
4935 'the current branch')
4936 parser.add_option(
4937 '-a',
4938 '--enable-auto-submit',
4939 action='store_true',
4940 help='Sends your change to the CQ after an approval. Only '
4941 'works on repos that have the Auto-Submit label '
4942 'enabled')
4943 parser.add_option(
4944 '--parallel',
4945 action='store_true',
4946 help='Run all tests specified by input_api.RunTests in all '
4947 'PRESUBMIT files in parallel.')
4948 parser.add_option('--no-autocc',
4949 action='store_true',
4950 help='Disables automatic addition of CC emails')
4951 parser.add_option('--private',
4952 action='store_true',
4953 help='Set the review private. This implies --no-autocc.')
4954 parser.add_option('-R',
4955 '--retry-failed',
4956 action='store_true',
4957 help='Retry failed tryjobs from old patchset immediately '
4958 'after uploading new patchset. Cannot be used with '
4959 '--use-commit-queue or --cq-dry-run.')
4960 parser.add_option('--fixed',
4961 '-x',
4962 help='List of bugs that will be commented on and marked '
4963 'fixed (pre-populates "Fixed:" tag). Same format as '
4964 '-b option / "Bug:" tag. If fixing several issues, '
4965 'separate with commas.')
4966 parser.add_option('--edit-description',
4967 action='store_true',
4968 default=False,
4969 help='Modify description before upload. Cannot be used '
4970 'with --force. It is a noop when --no-squash is set '
4971 'or a new commit is created.')
4972 parser.add_option('--git-completion-helper',
4973 action="store_true",
4974 help=optparse.SUPPRESS_HELP)
4975 parser.add_option('-o',
4976 '--push-options',
4977 action='append',
4978 default=[],
4979 help='Transmit the given string to the server when '
4980 'performing git push (pass-through). See git-push '
4981 'documentation for more details.')
4982 parser.add_option('--no-add-changeid',
4983 action='store_true',
4984 dest='no_add_changeid',
4985 help='Do not add change-ids to messages.')
4986 parser.add_option('--cherry-pick-stacked',
4987 '--cp',
4988 dest='cherry_pick_stacked',
4989 action='store_true',
4990 help='If parent branch has un-uploaded updates, '
4991 'automatically skip parent branches and just upload '
4992 'the current branch cherry-pick on its parent\'s last '
4993 'uploaded commit. Allows users to skip the potential '
4994 'interactive confirmation step.')
4995 # TODO(b/265929888): Add --wip option of --cl-status option.
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004996
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004997 orig_args = args
4998 (options, args) = parser.parse_args(args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004999
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005000 if options.git_completion_helper:
5001 print(' '.join(opt.get_opt_string() for opt in parser.option_list
5002 if opt.help != optparse.SUPPRESS_HELP))
5003 return
Ng Zhi Ancdaf0be2020-05-27 20:57:28 +00005004
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005005 # TODO(crbug.com/1475405): Warn users if the project uses submodules and
5006 # they have fsmonitor enabled.
5007 if os.path.isfile('.gitmodules'):
5008 git_common.warn_submodule()
Aravind Vasudevanb8164182023-08-25 21:49:12 +00005009
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005010 if git_common.is_dirty_git_tree('upload'):
5011 return 1
ukai@chromium.orge8077812012-02-03 03:41:46 +00005012
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005013 options.reviewers = cleanup_list(options.reviewers)
5014 options.cc = cleanup_list(options.cc)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005015
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005016 if options.edit_description and options.force:
5017 parser.error('Only one of --force and --edit-description allowed')
Josipe827b0f2020-01-30 00:07:20 +00005018
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005019 if options.message_file:
5020 if options.message:
5021 parser.error('Only one of --message and --message-file allowed.')
5022 options.message = gclient_utils.FileRead(options.message_file)
tandriib80458a2016-06-23 12:20:07 -07005023
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005024 if ([options.cq_dry_run, options.use_commit_queue, options.retry_failed
5025 ].count(True) > 1):
5026 parser.error('Only one of --use-commit-queue, --cq-dry-run or '
5027 '--retry-failed is allowed.')
tandrii4d0545a2016-07-06 03:56:49 -07005028
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005029 if options.skip_title and options.title:
5030 parser.error('Only one of --title and --skip-title allowed.')
Mario Bianuccicebfb4e2020-07-22 23:08:16 +00005031
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005032 if options.use_commit_queue:
5033 options.send_mail = True
Aaron Gableedbc4132017-09-11 13:22:28 -07005034
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005035 if options.squash is None:
5036 # Load default for user, repo, squash=true, in this order.
5037 options.squash = settings.GetSquashGerritUploads()
Edward Lesmes0dd54822020-03-26 18:24:25 +00005038
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005039 cl = Changelist(branchref=options.target_branch)
Joanna Wang5051ffe2023-03-01 22:24:07 +00005040
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005041 # Warm change details cache now to avoid RPCs later, reducing latency for
5042 # developers.
5043 if cl.GetIssue():
5044 cl._GetChangeDetail([
5045 'DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'
5046 ])
Joanna Wang5051ffe2023-03-01 22:24:07 +00005047
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005048 if options.retry_failed and not cl.GetIssue():
5049 print('No previous patchsets, so --retry-failed has no effect.')
5050 options.retry_failed = False
Joanna Wang5051ffe2023-03-01 22:24:07 +00005051
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005052 disable_dogfood_stacked_changes = os.environ.get(
5053 DOGFOOD_STACKED_CHANGES_VAR) == '0'
5054 dogfood_stacked_changes = os.environ.get(DOGFOOD_STACKED_CHANGES_VAR) == '1'
Joanna Wang5051ffe2023-03-01 22:24:07 +00005055
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005056 # Only print message for folks who don't have DOGFOOD_STACKED_CHANGES set
5057 # to an expected value.
5058 if (options.squash and not dogfood_stacked_changes
5059 and not disable_dogfood_stacked_changes):
5060 print(
5061 'This repo has been enrolled in the stacked changes dogfood.\n'
5062 '`git cl upload` now uploads the current branch and all upstream '
5063 'branches that have un-uploaded updates.\n'
5064 'Patches can now be reapplied with --force:\n'
5065 '`git cl patch --reapply --force`.\n'
5066 'Googlers may visit go/stacked-changes-dogfood for more information.\n'
5067 '\n'
5068 'Depot Tools no longer sets new uploads to "WIP". Please update the\n'
5069 '"Set new changes to "work in progress" by default" checkbox at\n'
5070 'https://<host>-review.googlesource.com/settings/\n'
5071 '\n'
5072 'To opt-out use `export DOGFOOD_STACKED_CHANGES=0`.\n'
5073 'To hide this message use `export DOGFOOD_STACKED_CHANGES=1`.\n'
5074 'File bugs at https://bit.ly/3Y6opoI\n')
Joanna Wang4786a412023-05-16 18:23:08 +00005075
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005076 if options.squash and not disable_dogfood_stacked_changes:
5077 if options.dependencies:
5078 parser.error(
5079 '--dependencies is not available for this dogfood workflow.')
Joanna Wang5051ffe2023-03-01 22:24:07 +00005080
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005081 if options.cherry_pick_stacked:
5082 try:
5083 orig_args.remove('--cherry-pick-stacked')
5084 except ValueError:
5085 orig_args.remove('--cp')
5086 UploadAllSquashed(options, orig_args)
5087 return 0
Joanna Wang18de1f62023-01-21 01:24:24 +00005088
Joanna Wangd75fc882023-03-01 21:53:34 +00005089 if options.cherry_pick_stacked:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005090 parser.error(
5091 '--cherry-pick-stacked is not available for this workflow.')
Joanna Wang18de1f62023-01-21 01:24:24 +00005092
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005093 # cl.GetMostRecentPatchset uses cached information, and can return the last
5094 # patchset before upload. Calling it here makes it clear that it's the
5095 # last patchset before upload. Note that GetMostRecentPatchset will fail
5096 # if no CL has been uploaded yet.
5097 if options.retry_failed:
5098 patchset = cl.GetMostRecentPatchset()
Joanna Wangd75fc882023-03-01 21:53:34 +00005099
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005100 ret = cl.CMDUpload(options, args, orig_args)
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00005101
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005102 if options.retry_failed:
5103 if ret != 0:
5104 print('Upload failed, so --retry-failed has no effect.')
5105 return ret
5106 builds, _ = _fetch_latest_builds(cl,
5107 DEFAULT_BUILDBUCKET_HOST,
5108 latest_patchset=patchset)
5109 jobs = _filter_failed_for_retry(builds)
5110 if len(jobs) == 0:
5111 print('No failed tryjobs, so --retry-failed has no effect.')
5112 return ret
5113 _trigger_tryjobs(cl, jobs, options, patchset + 1)
Quinten Yearsleya19d3532019-09-30 21:54:39 +00005114
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005115 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00005116
5117
Daniel Cheng66d0f152023-08-29 23:21:58 +00005118def UploadAllSquashed(options: optparse.Values,
5119 orig_args: Sequence[str]) -> int:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005120 """Uploads the current and upstream branches (if necessary)."""
5121 cls, cherry_pick_current = _UploadAllPrecheck(options, orig_args)
Joanna Wang18de1f62023-01-21 01:24:24 +00005122
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005123 # Create commits.
5124 uploads_by_cl: List[Tuple[Changelist, _NewUpload]] = []
5125 if cherry_pick_current:
5126 parent = cls[1]._GitGetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY)
5127 new_upload = cls[0].PrepareCherryPickSquashedCommit(options, parent)
5128 uploads_by_cl.append((cls[0], new_upload))
5129 else:
5130 ordered_cls = list(reversed(cls))
Joanna Wangc710e2d2023-01-25 14:53:22 +00005131
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005132 cl = ordered_cls[0]
5133 # We can only support external changes when we're only uploading one
5134 # branch.
5135 parent = cl._UpdateWithExternalChanges() if len(
5136 ordered_cls) == 1 else None
5137 orig_parent = None
5138 if parent is None:
5139 origin = '.'
5140 branch = cl.GetBranch()
Joanna Wang74c53b62023-03-01 22:00:22 +00005141
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005142 while origin == '.':
5143 # Search for cl's closest ancestor with a gerrit hash.
5144 origin, upstream_branch_ref = Changelist.FetchUpstreamTuple(
5145 branch)
5146 if origin == '.':
5147 upstream_branch = scm.GIT.ShortBranchName(
5148 upstream_branch_ref)
Joanna Wang7603f042023-03-01 22:17:36 +00005149
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005150 # Support the `git merge` and `git pull` workflow.
5151 if upstream_branch in ['master', 'main']:
5152 parent = cl.GetCommonAncestorWithUpstream()
5153 else:
5154 orig_parent = scm.GIT.GetBranchConfig(
5155 settings.GetRoot(), upstream_branch,
5156 LAST_UPLOAD_HASH_CONFIG_KEY)
5157 parent = scm.GIT.GetBranchConfig(
5158 settings.GetRoot(), upstream_branch,
5159 GERRIT_SQUASH_HASH_CONFIG_KEY)
5160 if parent:
5161 break
5162 branch = upstream_branch
5163 else:
5164 # Either the root of the tree is the cl's direct parent and the
5165 # while loop above only found empty branches between cl and the
5166 # root of the tree.
5167 parent = cl.GetCommonAncestorWithUpstream()
Joanna Wang6215dd02023-02-07 15:58:03 +00005168
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005169 if orig_parent is None:
5170 orig_parent = parent
5171 for i, cl in enumerate(ordered_cls):
5172 # If we're in the middle of the stack, set end_commit to
5173 # downstream's direct ancestor.
5174 if i + 1 < len(ordered_cls):
5175 child_base_commit = ordered_cls[
5176 i + 1].GetCommonAncestorWithUpstream()
5177 else:
5178 child_base_commit = None
5179 new_upload = cl.PrepareSquashedCommit(options,
5180 parent,
5181 orig_parent,
5182 end_commit=child_base_commit)
5183 uploads_by_cl.append((cl, new_upload))
5184 parent = new_upload.commit_to_push
5185 orig_parent = child_base_commit
Joanna Wangc710e2d2023-01-25 14:53:22 +00005186
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005187 # Create refspec options
5188 cl, new_upload = uploads_by_cl[-1]
5189 refspec_opts = cl._GetRefSpecOptions(
5190 options,
5191 new_upload.change_desc,
5192 multi_change_upload=len(uploads_by_cl) > 1,
5193 dogfood_path=True)
5194 refspec_suffix = ''
5195 if refspec_opts:
5196 refspec_suffix = '%' + ','.join(refspec_opts)
5197 assert ' ' not in refspec_suffix, (
5198 'spaces not allowed in refspec: "%s"' % refspec_suffix)
Joanna Wangc710e2d2023-01-25 14:53:22 +00005199
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005200 remote, remote_branch = cl.GetRemoteBranch()
5201 branch = GetTargetRef(remote, remote_branch, options.target_branch)
5202 refspec = '%s:refs/for/%s%s' % (new_upload.commit_to_push, branch,
5203 refspec_suffix)
Joanna Wangc710e2d2023-01-25 14:53:22 +00005204
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005205 # Git push
5206 git_push_metadata = {
5207 'gerrit_host':
5208 cl.GetGerritHost(),
5209 'title':
5210 options.title or '<untitled>',
5211 'change_id':
5212 git_footers.get_footer_change_id(new_upload.change_desc.description),
5213 'description':
5214 new_upload.change_desc.description,
5215 }
5216 push_stdout = cl._RunGitPushWithTraces(refspec, refspec_opts,
5217 git_push_metadata,
5218 options.push_options)
Joanna Wangc710e2d2023-01-25 14:53:22 +00005219
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005220 # Post push updates
5221 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
5222 change_numbers = [
5223 m.group(1) for m in map(regex.match, push_stdout.splitlines()) if m
5224 ]
Joanna Wangc710e2d2023-01-25 14:53:22 +00005225
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005226 for i, (cl, new_upload) in enumerate(uploads_by_cl):
5227 cl.PostUploadUpdates(options, new_upload, change_numbers[i])
Joanna Wangc710e2d2023-01-25 14:53:22 +00005228
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005229 return 0
Joanna Wang18de1f62023-01-21 01:24:24 +00005230
5231
5232def _UploadAllPrecheck(options, orig_args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005233 # type: (optparse.Values, Sequence[str]) -> Tuple[Sequence[Changelist],
5234 # bool]
5235 """Checks the state of the tree and gives the user uploading options
Joanna Wang18de1f62023-01-21 01:24:24 +00005236
5237 Returns: A tuple of the ordered list of changes that have new commits
5238 since their last upload and a boolean of whether the user wants to
5239 cherry-pick and upload the current branch instead of uploading all cls.
5240 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005241 cl = Changelist()
5242 if cl.GetBranch() is None:
5243 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
Joanna Wang6b98cdc2023-02-16 00:37:20 +00005244
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005245 branch_ref = None
5246 cls = []
5247 must_upload_upstream = False
5248 first_pass = True
Joanna Wang18de1f62023-01-21 01:24:24 +00005249
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005250 Changelist._GerritCommitMsgHookCheck(offer_removal=not options.force)
Joanna Wang18de1f62023-01-21 01:24:24 +00005251
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005252 while True:
5253 if len(cls) > _MAX_STACKED_BRANCHES_UPLOAD:
5254 DieWithError(
5255 'More than %s branches in the stack have not been uploaded.\n'
5256 'Are your branches in a misconfigured state?\n'
5257 'If not, please upload some upstream changes first.' %
5258 (_MAX_STACKED_BRANCHES_UPLOAD))
Joanna Wang18de1f62023-01-21 01:24:24 +00005259
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005260 cl = Changelist(branchref=branch_ref)
Joanna Wang18de1f62023-01-21 01:24:24 +00005261
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005262 # Only add CL if it has anything to commit.
5263 base_commit = cl.GetCommonAncestorWithUpstream()
5264 end_commit = RunGit(['rev-parse', cl.GetBranchRef()]).strip()
Joanna Wang6215dd02023-02-07 15:58:03 +00005265
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005266 commit_summary = _GetCommitCountSummary(base_commit, end_commit)
5267 if commit_summary:
5268 cls.append(cl)
5269 if (not first_pass and
5270 cl._GitGetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY)
5271 is None):
5272 # We are mid-stack and the user must upload their upstream
5273 # branches.
5274 must_upload_upstream = True
5275 print(f'Found change with {commit_summary}...')
5276 elif first_pass: # The current branch has nothing to commit. Exit.
5277 DieWithError('Branch %s has nothing to commit' % cl.GetBranch())
5278 # Else: A mid-stack branch has nothing to commit. We do not add it to
5279 # cls.
5280 first_pass = False
Joanna Wang6215dd02023-02-07 15:58:03 +00005281
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005282 # Cases below determine if we should continue to traverse up the tree.
5283 origin, upstream_branch_ref = Changelist.FetchUpstreamTuple(
5284 cl.GetBranch())
5285 branch_ref = upstream_branch_ref # set branch for next run.
Joanna Wang18de1f62023-01-21 01:24:24 +00005286
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005287 upstream_branch = scm.GIT.ShortBranchName(upstream_branch_ref)
5288 upstream_last_upload = scm.GIT.GetBranchConfig(
5289 settings.GetRoot(), upstream_branch, LAST_UPLOAD_HASH_CONFIG_KEY)
Joanna Wang6215dd02023-02-07 15:58:03 +00005290
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005291 # Case 1: We've reached the beginning of the tree.
5292 if origin != '.':
5293 break
Joanna Wang18de1f62023-01-21 01:24:24 +00005294
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005295 # Case 2: If any upstream branches have never been uploaded,
5296 # the user MUST upload them unless they are empty. Continue to
5297 # next loop to add upstream if it is not empty.
5298 if not upstream_last_upload:
5299 continue
Joanna Wang18de1f62023-01-21 01:24:24 +00005300
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005301 # Case 3: If upstream's last_upload == cl.base_commit we do
5302 # not need to upload any more upstreams from this point on.
5303 # (Even if there may be diverged branches higher up the tree)
5304 if base_commit == upstream_last_upload:
5305 break
Joanna Wang18de1f62023-01-21 01:24:24 +00005306
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005307 # Case 4: If upstream's last_upload < cl.base_commit we are
5308 # uploading cl and upstream_cl.
5309 # Continue up the tree to check other branch relations.
5310 if scm.GIT.IsAncestor(upstream_last_upload, base_commit):
5311 continue
Joanna Wang18de1f62023-01-21 01:24:24 +00005312
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005313 # Case 5: If cl.base_commit < upstream's last_upload the user
5314 # must rebase before uploading.
5315 if scm.GIT.IsAncestor(base_commit, upstream_last_upload):
5316 DieWithError(
5317 'At least one branch in the stack has diverged from its upstream '
5318 'branch and does not contain its upstream\'s last upload.\n'
5319 'Please rebase the stack with `git rebase-update` before uploading.'
5320 )
Joanna Wang18de1f62023-01-21 01:24:24 +00005321
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005322 # The tree went through a rebase. LAST_UPLOAD_HASH_CONFIG_KEY no longer
5323 # has any relation to commits in the tree. Continue up the tree until we
5324 # hit the root.
Joanna Wang18de1f62023-01-21 01:24:24 +00005325
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005326 # We assume all cls in the stack have the same auth requirements and only
5327 # check this once.
5328 cls[0].EnsureAuthenticated(force=options.force)
Joanna Wang18de1f62023-01-21 01:24:24 +00005329
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005330 cherry_pick = False
5331 if len(cls) > 1:
5332 opt_message = ''
5333 branches = ', '.join([cl.branch for cl in cls])
5334 if len(orig_args):
5335 opt_message = ('options %s will be used for all uploads.\n' %
5336 orig_args)
5337 if must_upload_upstream:
5338 msg = ('At least one parent branch in `%s` has never been uploaded '
5339 'and must be uploaded before/with `%s`.\n' %
5340 (branches, cls[1].branch))
5341 if options.cherry_pick_stacked:
5342 DieWithError(msg)
5343 if not options.force:
5344 confirm_or_exit('\n' + opt_message + msg)
5345 else:
5346 if options.cherry_pick_stacked:
5347 print('cherry-picking `%s` on %s\'s last upload' %
5348 (cls[0].branch, cls[1].branch))
5349 cherry_pick = True
5350 elif not options.force:
5351 answer = gclient_utils.AskForData(
5352 '\n' + opt_message +
5353 'Press enter to update branches %s.\nOr type `n` to upload only '
5354 '`%s` cherry-picked on %s\'s last upload:' %
5355 (branches, cls[0].branch, cls[1].branch))
5356 if answer.lower() == 'n':
5357 cherry_pick = True
5358 return cls, cherry_pick
Joanna Wang18de1f62023-01-21 01:24:24 +00005359
5360
Francois Dorayd42c6812017-05-30 15:10:20 -04005361@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005362@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04005363def CMDsplit(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005364 """Splits a branch into smaller branches and uploads CLs.
Francois Dorayd42c6812017-05-30 15:10:20 -04005365
5366 Creates a branch and uploads a CL for each group of files modified in the
5367 current branch that share a common OWNERS file. In the CL description and
Edward Lemurac5c55f2020-02-29 00:17:16 +00005368 comment, the string '$directory', is replaced with the directory containing
5369 the shared OWNERS file.
Francois Dorayd42c6812017-05-30 15:10:20 -04005370 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005371 parser.add_option('-d',
5372 '--description',
5373 dest='description_file',
5374 help='A text file containing a CL description in which '
5375 '$directory will be replaced by each CL\'s directory.')
5376 parser.add_option('-c',
5377 '--comment',
5378 dest='comment_file',
5379 help='A text file containing a CL comment.')
5380 parser.add_option(
5381 '-n',
5382 '--dry-run',
5383 dest='dry_run',
5384 action='store_true',
5385 default=False,
5386 help='List the files and reviewers for each CL that would '
5387 'be created, but don\'t create branches or CLs.')
5388 parser.add_option('--cq-dry-run',
5389 action='store_true',
5390 help='If set, will do a cq dry run for each uploaded CL. '
5391 'Please be careful when doing this; more than ~10 CLs '
5392 'has the potential to overload our build '
5393 'infrastructure. Try to upload these not during high '
5394 'load times (usually 11-3 Mountain View time). Email '
5395 'infra-dev@chromium.org with any questions.')
5396 parser.add_option(
5397 '-a',
5398 '--enable-auto-submit',
5399 action='store_true',
5400 dest='enable_auto_submit',
5401 default=True,
5402 help='Sends your change to the CQ after an approval. Only '
5403 'works on repos that have the Auto-Submit label '
5404 'enabled')
5405 parser.add_option(
5406 '--disable-auto-submit',
5407 action='store_false',
5408 dest='enable_auto_submit',
5409 help='Disables automatic sending of the changes to the CQ '
5410 'after approval. Note that auto-submit only works for '
5411 'repos that have the Auto-Submit label enabled.')
5412 parser.add_option('--max-depth',
5413 type='int',
5414 default=0,
5415 help='The max depth to look for OWNERS files. Useful for '
5416 'controlling the granularity of the split CLs, e.g. '
5417 '--max-depth=1 will only split by top-level '
5418 'directory. Specifying a value less than 1 means no '
5419 'limit on max depth.')
5420 parser.add_option('--topic',
5421 default=None,
5422 help='Topic to specify when uploading')
5423 options, _ = parser.parse_args(args)
Francois Dorayd42c6812017-05-30 15:10:20 -04005424
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005425 if not options.description_file:
5426 parser.error('No --description flag specified.')
Francois Dorayd42c6812017-05-30 15:10:20 -04005427
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005428 def WrappedCMDupload(args):
5429 return CMDupload(OptionParser(), args)
Francois Dorayd42c6812017-05-30 15:10:20 -04005430
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005431 return split_cl.SplitCl(options.description_file, options.comment_file,
5432 Changelist, WrappedCMDupload, options.dry_run,
5433 options.cq_dry_run, options.enable_auto_submit,
5434 options.max_depth, options.topic,
5435 settings.GetRoot())
Francois Dorayd42c6812017-05-30 15:10:20 -04005436
5437
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005438@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005439@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005440def CMDdcommit(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005441 """DEPRECATED: Used to commit the current changelist via git-svn."""
5442 message = ('git-cl no longer supports committing to SVN repositories via '
5443 'git-svn. You probably want to use `git cl land` instead.')
5444 print(message)
5445 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005446
5447
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005448@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005449@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00005450def CMDland(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005451 """Commits the current changelist via git.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005452
5453 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
5454 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005455 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005456 parser.add_option('--bypass-hooks',
5457 action='store_true',
5458 dest='bypass_hooks',
5459 help='bypass upload presubmit hook')
5460 parser.add_option('-f',
5461 '--force',
5462 action='store_true',
5463 dest='force',
5464 help="force yes to questions (don't prompt)")
5465 parser.add_option(
5466 '--parallel',
5467 action='store_true',
5468 help='Run all tests specified by input_api.RunTests in all '
5469 'PRESUBMIT files in parallel.')
5470 parser.add_option('--resultdb',
5471 action='store_true',
5472 help='Run presubmit checks in the ResultSink environment '
5473 'and send results to the ResultDB database.')
5474 parser.add_option('--realm', help='LUCI realm if reporting to ResultDB')
5475 (options, args) = parser.parse_args(args)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005476
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005477 cl = Changelist()
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005478
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005479 if not cl.GetIssue():
5480 DieWithError('You must upload the change first to Gerrit.\n'
5481 ' If you would rather have `git cl land` upload '
5482 'automatically for you, see http://crbug.com/642759')
5483 return cl.CMDLand(options.force, options.bypass_hooks, options.verbose,
5484 options.parallel, options.resultdb, options.realm)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005485
5486
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005487@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005488@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005489def CMDpatch(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005490 """Applies (cherry-picks) a Gerrit changelist locally."""
5491 parser.add_option('-b',
5492 dest='newbranch',
5493 help='create a new branch off trunk for the patch')
5494 parser.add_option('-f',
5495 '--force',
5496 action='store_true',
5497 help='overwrite state on the current or chosen branch')
5498 parser.add_option('-n',
5499 '--no-commit',
5500 action='store_true',
5501 dest='nocommit',
5502 help='don\'t commit after patch applies.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005503
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005504 group = optparse.OptionGroup(
5505 parser,
5506 'Options for continuing work on the current issue uploaded from a '
5507 'different clone (e.g. different machine). Must be used independently '
5508 'from the other options. No issue number should be specified, and the '
5509 'branch must have an issue number associated with it')
5510 group.add_option('--reapply',
5511 action='store_true',
5512 dest='reapply',
5513 help='Reset the branch and reapply the issue.\n'
5514 'CAUTION: This will undo any local changes in this '
5515 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005516
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005517 group.add_option('--pull',
5518 action='store_true',
5519 dest='pull',
5520 help='Performs a pull before reapplying.')
5521 parser.add_option_group(group)
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005522
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005523 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005524
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005525 if options.reapply:
5526 if options.newbranch:
5527 parser.error('--reapply works on the current branch only.')
5528 if len(args) > 0:
5529 parser.error('--reapply implies no additional arguments.')
5530
5531 cl = Changelist()
5532 if not cl.GetIssue():
5533 parser.error('Current branch must have an associated issue.')
5534
5535 upstream = cl.GetUpstreamBranch()
5536 if upstream is None:
5537 parser.error('No upstream branch specified. Cannot reset branch.')
5538
5539 RunGit(['reset', '--hard', upstream])
5540 if options.pull:
5541 RunGit(['pull'])
5542
5543 target_issue_arg = ParseIssueNumberArgument(cl.GetIssue())
5544 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.nocommit,
5545 options.force, False)
5546
5547 if len(args) != 1 or not args[0]:
5548 parser.error('Must specify issue number or URL.')
5549
5550 target_issue_arg = ParseIssueNumberArgument(args[0])
5551 if not target_issue_arg.valid:
5552 parser.error('Invalid issue ID or URL.')
5553
5554 # We don't want uncommitted changes mixed up with the patch.
5555 if git_common.is_dirty_git_tree('patch'):
5556 return 1
5557
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005558 if options.newbranch:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005559 if options.force:
5560 RunGit(['branch', '-D', options.newbranch],
5561 stderr=subprocess2.PIPE,
5562 error_ok=True)
5563 git_new_branch.create_new_branch(options.newbranch)
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005564
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005565 cl = Changelist(codereview_host=target_issue_arg.hostname,
5566 issue=target_issue_arg.issue)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005567
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005568 if not args[0].isdigit():
5569 print('canonical issue/change URL: %s\n' % cl.GetIssueURL())
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005570
Joanna Wang44e9bee2023-01-25 21:51:42 +00005571 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.nocommit,
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005572 options.force, options.newbranch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005573
5574
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005575def GetTreeStatus(url=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005576 """Fetches the tree status and returns either 'open', 'closed',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005577 'unknown' or 'unset'."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005578 url = url or settings.GetTreeStatusUrl(error_ok=True)
5579 if url:
5580 status = str(urllib.request.urlopen(url).read().lower())
5581 if status.find('closed') != -1 or status == '0':
5582 return 'closed'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00005583
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005584 if status.find('open') != -1 or status == '1':
5585 return 'open'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00005586
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005587 return 'unknown'
5588 return 'unset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005589
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005590
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005591def GetTreeStatusReason():
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005592 """Fetches the tree status from a json url and returns the message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005593 with the reason for the tree to be opened or closed."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005594 url = settings.GetTreeStatusUrl()
5595 json_url = urllib.parse.urljoin(url, '/current?format=json')
5596 connection = urllib.request.urlopen(json_url)
5597 status = json.loads(connection.read())
5598 connection.close()
5599 return status['message']
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005600
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005601
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005602@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005603def CMDtree(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005604 """Shows the status of the tree."""
5605 _, args = parser.parse_args(args)
5606 status = GetTreeStatus()
5607 if 'unset' == status:
5608 print(
5609 'You must configure your tree status URL by running "git cl config".'
5610 )
5611 return 2
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005612
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005613 print('The tree is %s' % status)
5614 print()
5615 print(GetTreeStatusReason())
5616 if status != 'open':
5617 return 1
5618 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005619
5620
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005621@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00005622def CMDtry(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005623 """Triggers tryjobs using either Buildbucket or CQ dry run."""
5624 group = optparse.OptionGroup(parser, 'Tryjob options')
5625 group.add_option(
5626 '-b',
5627 '--bot',
5628 action='append',
5629 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5630 'times to specify multiple builders. ex: '
5631 '"-b win_rel -b win_layout". See '
5632 'the try server waterfall for the builders name and the tests '
5633 'available.'))
5634 group.add_option(
5635 '-B',
5636 '--bucket',
5637 default='',
5638 help=('Buildbucket bucket to send the try requests. Format: '
5639 '"luci.$LUCI_PROJECT.$LUCI_BUCKET". eg: "luci.chromium.try"'))
5640 group.add_option(
5641 '-r',
5642 '--revision',
5643 help='Revision to use for the tryjob; default: the revision will '
5644 'be determined by the try recipe that builder runs, which usually '
5645 'defaults to HEAD of origin/master or origin/main')
5646 group.add_option(
5647 '-c',
5648 '--clobber',
5649 action='store_true',
5650 default=False,
5651 help='Force a clobber before building; that is don\'t do an '
5652 'incremental build')
5653 group.add_option('--category',
5654 default='git_cl_try',
5655 help='Specify custom build category.')
5656 group.add_option(
5657 '--project',
5658 help='Override which project to use. Projects are defined '
5659 'in recipe to determine to which repository or directory to '
5660 'apply the patch')
5661 group.add_option(
5662 '-p',
5663 '--property',
5664 dest='properties',
5665 action='append',
5666 default=[],
5667 help='Specify generic properties in the form -p key1=value1 -p '
5668 'key2=value2 etc. The value will be treated as '
5669 'json if decodable, or as string otherwise. '
5670 'NOTE: using this may make your tryjob not usable for CQ, '
5671 'which will then schedule another tryjob with default properties')
5672 group.add_option('--buildbucket-host',
5673 default='cr-buildbucket.appspot.com',
5674 help='Host of buildbucket. The default host is %default.')
5675 parser.add_option_group(group)
5676 parser.add_option('-R',
5677 '--retry-failed',
5678 action='store_true',
5679 default=False,
5680 help='Retry failed jobs from the latest set of tryjobs. '
5681 'Not allowed with --bucket and --bot options.')
5682 parser.add_option(
5683 '-i',
5684 '--issue',
5685 type=int,
5686 help='Operate on this issue instead of the current branch\'s implicit '
5687 'issue.')
5688 options, args = parser.parse_args(args)
maruel@chromium.org15192402012-09-06 12:38:29 +00005689
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005690 # Make sure that all properties are prop=value pairs.
5691 bad_params = [x for x in options.properties if '=' not in x]
5692 if bad_params:
5693 parser.error('Got properties with missing "=": %s' % bad_params)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005694
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005695 if args:
5696 parser.error('Unknown arguments: %s' % args)
maruel@chromium.org15192402012-09-06 12:38:29 +00005697
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005698 cl = Changelist(issue=options.issue)
5699 if not cl.GetIssue():
5700 parser.error('Need to upload first.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005701
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005702 # HACK: warm up Gerrit change detail cache to save on RPCs.
5703 cl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005704
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005705 error_message = cl.CannotTriggerTryJobReason()
5706 if error_message:
5707 parser.error('Can\'t trigger tryjobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005708
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005709 if options.bot:
5710 if options.retry_failed:
5711 parser.error('--bot is not compatible with --retry-failed.')
5712 if not options.bucket:
5713 parser.error('A bucket (e.g. "chromium/try") is required.')
Edward Lemur45768512020-03-02 19:03:14 +00005714
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005715 triggered = [b for b in options.bot if 'triggered' in b]
5716 if triggered:
5717 parser.error(
5718 'Cannot schedule builds on triggered bots: %s.\n'
5719 'This type of bot requires an initial job from a parent (usually a '
5720 'builder). Schedule a job on the parent instead.\n' % triggered)
Edward Lemur45768512020-03-02 19:03:14 +00005721
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005722 if options.bucket.startswith('.master'):
5723 parser.error('Buildbot masters are not supported.')
Edward Lemur45768512020-03-02 19:03:14 +00005724
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005725 project, bucket = _parse_bucket(options.bucket)
5726 if project is None or bucket is None:
5727 parser.error('Invalid bucket: %s.' % options.bucket)
5728 jobs = sorted((project, bucket, bot) for bot in options.bot)
5729 elif options.retry_failed:
5730 print('Searching for failed tryjobs...')
5731 builds, patchset = _fetch_latest_builds(cl, DEFAULT_BUILDBUCKET_HOST)
5732 if options.verbose:
5733 print('Got %d builds in patchset #%d' % (len(builds), patchset))
5734 jobs = _filter_failed_for_retry(builds)
5735 if not jobs:
5736 print('There are no failed jobs in the latest set of jobs '
5737 '(patchset #%d), doing nothing.' % patchset)
5738 return 0
5739 num_builders = len(jobs)
5740 if num_builders > 10:
5741 confirm_or_exit('There are %d builders with failed builds.' %
5742 num_builders,
5743 action='continue')
5744 else:
5745 if options.verbose:
5746 print('git cl try with no bots now defaults to CQ dry run.')
5747 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5748 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005749
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005750 patchset = cl.GetMostRecentPatchset()
5751 try:
5752 _trigger_tryjobs(cl, jobs, options, patchset)
5753 except BuildbucketResponseException as ex:
5754 print('ERROR: %s' % ex)
5755 return 1
5756 return 0
maruel@chromium.org15192402012-09-06 12:38:29 +00005757
5758
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005759@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005760def CMDtry_results(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005761 """Prints info about results for tryjobs associated with the current CL."""
5762 group = optparse.OptionGroup(parser, 'Tryjob results options')
5763 group.add_option('-p',
5764 '--patchset',
5765 type=int,
5766 help='patchset number if not current.')
5767 group.add_option('--print-master',
5768 action='store_true',
5769 help='print master name as well.')
5770 group.add_option('--color',
5771 action='store_true',
5772 default=setup_color.IS_TTY,
5773 help='force color output, useful when piping output.')
5774 group.add_option('--buildbucket-host',
5775 default='cr-buildbucket.appspot.com',
5776 help='Host of buildbucket. The default host is %default.')
5777 group.add_option(
5778 '--json',
5779 help=('Path of JSON output file to write tryjob results to,'
5780 'or "-" for stdout.'))
5781 parser.add_option_group(group)
5782 parser.add_option(
5783 '-i',
5784 '--issue',
5785 type=int,
5786 help='Operate on this issue instead of the current branch\'s implicit '
5787 'issue.')
5788 options, args = parser.parse_args(args)
5789 if args:
5790 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005791
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005792 cl = Changelist(issue=options.issue)
5793 if not cl.GetIssue():
5794 parser.error('Need to upload first.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005795
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005796 patchset = options.patchset
tandrii221ab252016-10-06 08:12:04 -07005797 if not patchset:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005798 patchset = cl.GetMostRecentDryRunPatchset()
5799 if not patchset:
5800 parser.error('Code review host doesn\'t know about issue %s. '
5801 'No access to issue or wrong issue number?\n'
5802 'Either upload first, or pass --patchset explicitly.' %
5803 cl.GetIssue())
tandrii221ab252016-10-06 08:12:04 -07005804
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005805 try:
5806 jobs = _fetch_tryjobs(cl, DEFAULT_BUILDBUCKET_HOST, patchset)
5807 except BuildbucketResponseException as ex:
5808 print('Buildbucket error: %s' % ex)
5809 return 1
5810 if options.json:
5811 write_json(options.json, jobs)
5812 else:
5813 _print_tryjobs(options, jobs)
5814 return 0
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005815
5816
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005817@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005818@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005819def CMDupstream(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005820 """Prints or sets the name of the upstream branch, if any."""
5821 _, args = parser.parse_args(args)
5822 if len(args) > 1:
5823 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005824
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005825 cl = Changelist()
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005826 if args:
5827 # One arg means set upstream branch.
5828 branch = cl.GetBranch()
5829 RunGit(['branch', '--set-upstream-to', args[0], branch])
5830 cl = Changelist()
5831 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(), ))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005832
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005833 # Clear configured merge-base, if there is one.
5834 git_common.remove_merge_base(branch)
5835 else:
5836 print(cl.GetUpstreamBranch())
5837 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005838
5839
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005840@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00005841def CMDweb(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005842 """Opens the current CL in the web browser."""
5843 parser.add_option('-p',
5844 '--print-only',
5845 action='store_true',
5846 dest='print_only',
5847 help='Only print the Gerrit URL, don\'t open it in the '
5848 'browser.')
5849 (options, args) = parser.parse_args(args)
5850 if args:
5851 parser.error('Unrecognized args: %s' % ' '.join(args))
thestig@chromium.org00858c82013-12-02 23:08:03 +00005852
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005853 issue_url = Changelist().GetIssueURL()
5854 if not issue_url:
5855 print('ERROR No issue to open', file=sys.stderr)
5856 return 1
thestig@chromium.org00858c82013-12-02 23:08:03 +00005857
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005858 if options.print_only:
5859 print(issue_url)
5860 return 0
5861
5862 # Redirect I/O before invoking browser to hide its output. For example, this
5863 # allows us to hide the "Created new window in existing browser session."
5864 # message from Chrome. Based on https://stackoverflow.com/a/2323563.
5865 saved_stdout = os.dup(1)
5866 saved_stderr = os.dup(2)
5867 os.close(1)
5868 os.close(2)
5869 os.open(os.devnull, os.O_RDWR)
5870 try:
5871 webbrowser.open(issue_url)
5872 finally:
5873 os.dup2(saved_stdout, 1)
5874 os.dup2(saved_stderr, 2)
Orr Bernstein0b960582022-12-22 20:16:18 +00005875 return 0
5876
thestig@chromium.org00858c82013-12-02 23:08:03 +00005877
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005878@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005879def CMDset_commit(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005880 """Sets the commit bit to trigger the CQ."""
5881 parser.add_option('-d',
5882 '--dry-run',
5883 action='store_true',
5884 help='trigger in dry run mode')
5885 parser.add_option('-c',
5886 '--clear',
5887 action='store_true',
5888 help='stop CQ run, if any')
5889 parser.add_option(
5890 '-i',
5891 '--issue',
5892 type=int,
5893 help='Operate on this issue instead of the current branch\'s implicit '
5894 'issue.')
5895 options, args = parser.parse_args(args)
5896 if args:
5897 parser.error('Unrecognized args: %s' % ' '.join(args))
5898 if [options.dry_run, options.clear].count(True) > 1:
5899 parser.error('Only one of --dry-run, and --clear are allowed.')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005900
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005901 cl = Changelist(issue=options.issue)
5902 if not cl.GetIssue():
5903 parser.error('Must upload the issue first.')
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00005904
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005905 if options.clear:
5906 state = _CQState.NONE
5907 elif options.dry_run:
5908 state = _CQState.DRY_RUN
5909 else:
5910 state = _CQState.COMMIT
5911 cl.SetCQState(state)
5912 return 0
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005913
5914
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005915@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005916def CMDset_close(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005917 """Closes the issue."""
5918 parser.add_option(
5919 '-i',
5920 '--issue',
5921 type=int,
5922 help='Operate on this issue instead of the current branch\'s implicit '
5923 'issue.')
5924 options, args = parser.parse_args(args)
5925 if args:
5926 parser.error('Unrecognized args: %s' % ' '.join(args))
5927 cl = Changelist(issue=options.issue)
5928 # Ensure there actually is an issue to close.
5929 if not cl.GetIssue():
5930 DieWithError('ERROR: No issue to close.')
5931 cl.CloseIssue()
5932 return 0
groby@chromium.org411034a2013-02-26 15:12:01 +00005933
5934
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005935@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005936def CMDdiff(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005937 """Shows differences between local tree and last upload."""
5938 parser.add_option('--stat',
5939 action='store_true',
5940 dest='stat',
5941 help='Generate a diffstat')
5942 options, args = parser.parse_args(args)
5943 if args:
5944 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005945
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005946 cl = Changelist()
5947 issue = cl.GetIssue()
5948 branch = cl.GetBranch()
5949 if not issue:
5950 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005951
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005952 base = cl._GitGetBranchConfigValue(LAST_UPLOAD_HASH_CONFIG_KEY)
5953 if not base:
5954 base = cl._GitGetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY)
5955 if not base:
5956 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5957 revision_info = detail['revisions'][detail['current_revision']]
5958 fetch_info = revision_info['fetch']['http']
5959 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5960 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005961
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005962 cmd = ['git', 'diff']
5963 if options.stat:
5964 cmd.append('--stat')
5965 cmd.append(base)
5966 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005967
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005968 return 0
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005969
5970
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005971@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005972def CMDowners(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005973 """Finds potential owners for reviewing."""
5974 parser.add_option(
5975 '--ignore-current',
5976 action='store_true',
5977 help='Ignore the CL\'s current reviewers and start from scratch.')
5978 parser.add_option('--ignore-self',
5979 action='store_true',
5980 help='Do not consider CL\'s author as an owners.')
5981 parser.add_option('--no-color',
5982 action='store_true',
5983 help='Use this option to disable color output')
5984 parser.add_option('--batch',
5985 action='store_true',
5986 help='Do not run interactively, just suggest some')
5987 # TODO: Consider moving this to another command, since other
5988 # git-cl owners commands deal with owners for a given CL.
5989 parser.add_option('--show-all',
5990 action='store_true',
5991 help='Show all owners for a particular file')
5992 options, args = parser.parse_args(args)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005993
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005994 cl = Changelist()
5995 author = cl.GetAuthor()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005996
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005997 if options.show_all:
5998 if len(args) == 0:
5999 print('No files specified for --show-all. Nothing to do.')
6000 return 0
6001 owners_by_path = cl.owners_client.BatchListOwners(args)
6002 for path in args:
6003 print('Owners for %s:' % path)
6004 print('\n'.join(
6005 ' - %s' % owner
6006 for owner in owners_by_path.get(path, ['No owners found'])))
6007 return 0
Yang Guo6e269a02019-06-26 11:17:02 +00006008
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006009 if args:
6010 if len(args) > 1:
6011 parser.error('Unknown args.')
6012 base_branch = args[0]
6013 else:
6014 # Default to diffing against the common ancestor of the upstream branch.
6015 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00006016
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006017 affected_files = cl.GetAffectedFiles(base_branch)
Dirk Prankebf980882017-09-02 15:08:00 -07006018
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006019 if options.batch:
6020 owners = cl.owners_client.SuggestOwners(affected_files,
6021 exclude=[author])
6022 print('\n'.join(owners))
6023 return 0
Dirk Prankebf980882017-09-02 15:08:00 -07006024
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006025 return owners_finder.OwnersFinder(
6026 affected_files,
6027 author, [] if options.ignore_current else cl.GetReviewers(),
6028 cl.owners_client,
6029 disable_color=options.no_color,
6030 ignore_author=options.ignore_self).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00006031
6032
Aiden Bennerc08566e2018-10-03 17:52:42 +00006033def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006034 """Generates a diff command."""
6035 # Generate diff for the current branch's changes.
6036 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
Aiden Bennerc08566e2018-10-03 17:52:42 +00006037
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006038 if allow_prefix:
6039 # explicitly setting --src-prefix and --dst-prefix is necessary in the
6040 # case that diff.noprefix is set in the user's git config.
6041 diff_cmd += ['--src-prefix=a/', '--dst-prefix=b/']
6042 else:
6043 diff_cmd += ['--no-prefix']
Aiden Bennerc08566e2018-10-03 17:52:42 +00006044
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006045 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006046
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006047 if args:
6048 for arg in args:
6049 if os.path.isdir(arg) or os.path.isfile(arg):
6050 diff_cmd.append(arg)
6051 else:
6052 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006053
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006054 return diff_cmd
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006055
Andrii Shyshkalov18975322017-01-25 16:44:13 +01006056
Jamie Madill5e96ad12020-01-13 16:08:35 +00006057def _RunClangFormatDiff(opts, clang_diff_files, top_dir, upstream_commit):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006058 """Runs clang-format-diff and sets a return value if necessary."""
Jamie Madill5e96ad12020-01-13 16:08:35 +00006059
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006060 if not clang_diff_files:
6061 return 0
Jamie Madill5e96ad12020-01-13 16:08:35 +00006062
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006063 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
6064 # formatted. This is used to block during the presubmit.
6065 return_value = 0
Jamie Madill5e96ad12020-01-13 16:08:35 +00006066
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006067 # Locate the clang-format binary in the checkout
Jamie Madill5e96ad12020-01-13 16:08:35 +00006068 try:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006069 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
Jamie Madill5e96ad12020-01-13 16:08:35 +00006070 except clang_format.NotFoundError as e:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006071 DieWithError(e)
Jamie Madill5e96ad12020-01-13 16:08:35 +00006072
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006073 if opts.full or settings.GetFormatFullByDefault():
6074 cmd = [clang_format_tool]
6075 if not opts.dry_run and not opts.diff:
6076 cmd.append('-i')
6077 if opts.dry_run:
6078 for diff_file in clang_diff_files:
6079 with open(diff_file, 'r') as myfile:
6080 code = myfile.read().replace('\r\n', '\n')
6081 stdout = RunCommand(cmd + [diff_file], cwd=top_dir)
6082 stdout = stdout.replace('\r\n', '\n')
6083 if opts.diff:
6084 sys.stdout.write(stdout)
6085 if code != stdout:
6086 return_value = 2
6087 else:
6088 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
6089 if opts.diff:
6090 sys.stdout.write(stdout)
6091 else:
6092 try:
6093 script = clang_format.FindClangFormatScriptInChromiumTree(
6094 'clang-format-diff.py')
6095 except clang_format.NotFoundError as e:
6096 DieWithError(e)
Jamie Madill5e96ad12020-01-13 16:08:35 +00006097
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006098 cmd = ['vpython3', script, '-p0']
6099 if not opts.dry_run and not opts.diff:
6100 cmd.append('-i')
Jamie Madill5e96ad12020-01-13 16:08:35 +00006101
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006102 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
6103 diff_output = RunGit(diff_cmd).encode('utf-8')
Jamie Madill5e96ad12020-01-13 16:08:35 +00006104
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006105 env = os.environ.copy()
6106 env['PATH'] = (str(os.path.dirname(clang_format_tool)) + os.pathsep +
6107 env['PATH'])
6108 stdout = RunCommand(cmd,
6109 stdin=diff_output,
6110 cwd=top_dir,
6111 env=env,
6112 shell=sys.platform.startswith('win32'))
6113 if opts.diff:
6114 sys.stdout.write(stdout)
6115 if opts.dry_run and len(stdout) > 0:
6116 return_value = 2
6117
6118 return return_value
Jamie Madill5e96ad12020-01-13 16:08:35 +00006119
6120
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006121def _RunRustFmt(opts, rust_diff_files, top_dir, upstream_commit):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006122 """Runs rustfmt. Just like _RunClangFormatDiff returns 2 to indicate that
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006123 presubmit checks have failed (and returns 0 otherwise)."""
6124
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006125 if not rust_diff_files:
6126 return 0
6127
6128 # Locate the rustfmt binary.
6129 try:
6130 rustfmt_tool = rustfmt.FindRustfmtToolInChromiumTree()
6131 except rustfmt.NotFoundError as e:
6132 DieWithError(e)
6133
6134 # TODO(crbug.com/1440869): Support formatting only the changed lines
6135 # if `opts.full or settings.GetFormatFullByDefault()` is False.
6136 cmd = [rustfmt_tool]
6137 if opts.dry_run:
6138 cmd.append('--check')
6139 cmd += rust_diff_files
6140 rustfmt_exitcode = subprocess2.call(cmd)
6141
6142 if opts.presubmit and rustfmt_exitcode != 0:
6143 return 2
6144
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006145 return 0
6146
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006147
Olivier Robin0a6b5442022-04-07 07:25:04 +00006148def _RunSwiftFormat(opts, swift_diff_files, top_dir, upstream_commit):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006149 """Runs swift-format. Just like _RunClangFormatDiff returns 2 to indicate
Olivier Robin0a6b5442022-04-07 07:25:04 +00006150 that presubmit checks have failed (and returns 0 otherwise)."""
6151
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006152 if not swift_diff_files:
6153 return 0
6154
6155 # Locate the swift-format binary.
6156 try:
6157 swift_format_tool = swift_format.FindSwiftFormatToolInChromiumTree()
6158 except swift_format.NotFoundError as e:
6159 DieWithError(e)
6160
6161 cmd = [swift_format_tool]
6162 if opts.dry_run:
6163 cmd += ['lint', '-s']
6164 else:
6165 cmd += ['format', '-i']
6166 cmd += swift_diff_files
6167 swift_format_exitcode = subprocess2.call(cmd)
6168
6169 if opts.presubmit and swift_format_exitcode != 0:
6170 return 2
6171
Olivier Robin0a6b5442022-04-07 07:25:04 +00006172 return 0
6173
Olivier Robin0a6b5442022-04-07 07:25:04 +00006174
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006175def MatchingFileType(file_name, extensions):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006176 """Returns True if the file name ends with one of the given extensions."""
6177 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006178
Andrii Shyshkalov18975322017-01-25 16:44:13 +01006179
enne@chromium.org555cfe42014-01-29 18:21:39 +00006180@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006181@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006182def CMDformat(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006183 """Runs auto-formatting tools (clang-format etc.) on the diff."""
6184 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
6185 GN_EXTS = ['.gn', '.gni', '.typemap']
6186 RUST_EXTS = ['.rs']
6187 SWIFT_EXTS = ['.swift']
6188 parser.add_option('--full',
6189 action='store_true',
6190 help='Reformat the full content of all touched files')
6191 parser.add_option('--upstream', help='Branch to check against')
6192 parser.add_option('--dry-run',
6193 action='store_true',
6194 help='Don\'t modify any file on disk.')
6195 parser.add_option(
6196 '--no-clang-format',
6197 dest='clang_format',
6198 action='store_false',
6199 default=True,
6200 help='Disables formatting of various file types using clang-format.')
6201 parser.add_option('--python',
6202 action='store_true',
6203 default=None,
6204 help='Enables python formatting on all python files.')
6205 parser.add_option(
6206 '--no-python',
6207 action='store_true',
6208 default=False,
6209 help='Disables python formatting on all python files. '
6210 'If neither --python or --no-python are set, python files that have a '
6211 '.style.yapf file in an ancestor directory will be formatted. '
6212 'It is an error to set both.')
6213 parser.add_option('--js',
6214 action='store_true',
6215 help='Format javascript code with clang-format. '
6216 'Has no effect if --no-clang-format is set.')
6217 parser.add_option('--diff',
6218 action='store_true',
6219 help='Print diff to stdout rather than modifying files.')
6220 parser.add_option('--presubmit',
6221 action='store_true',
6222 help='Used when running the script from a presubmit.')
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006223
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006224 parser.add_option(
6225 '--rust-fmt',
6226 dest='use_rust_fmt',
6227 action='store_true',
6228 default=rustfmt.IsRustfmtSupported(),
6229 help='Enables formatting of Rust file types using rustfmt.')
6230 parser.add_option(
6231 '--no-rust-fmt',
6232 dest='use_rust_fmt',
6233 action='store_false',
6234 help='Disables formatting of Rust file types using rustfmt.')
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006235
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006236 parser.add_option(
6237 '--swift-format',
6238 dest='use_swift_format',
6239 action='store_true',
6240 default=swift_format.IsSwiftFormatSupported(),
6241 help='Enables formatting of Swift file types using swift-format '
6242 '(macOS host only).')
6243 parser.add_option(
6244 '--no-swift-format',
6245 dest='use_swift_format',
6246 action='store_false',
6247 help='Disables formatting of Swift file types using swift-format.')
Olivier Robin0a6b5442022-04-07 07:25:04 +00006248
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006249 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006250
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006251 if opts.python is not None and opts.no_python:
6252 raise parser.error('Cannot set both --python and --no-python')
6253 if opts.no_python:
6254 opts.python = False
Garrett Beaty91a6f332020-01-06 16:57:24 +00006255
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006256 # Normalize any remaining args against the current path, so paths relative
6257 # to the current directory are still resolved as expected.
6258 args = [os.path.join(os.getcwd(), arg) for arg in args]
Daniel Chengc55eecf2016-12-30 03:11:02 -08006259
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006260 # git diff generates paths against the root of the repository. Change
6261 # to that directory so clang-format can find files even within subdirs.
6262 rel_base_path = settings.GetRelativeRoot()
6263 if rel_base_path:
6264 os.chdir(rel_base_path)
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00006265
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006266 # Grab the merge-base commit, i.e. the upstream commit of the current
6267 # branch when it was created or the last time it was rebased. This is
6268 # to cover the case where the user may have called "git fetch origin",
6269 # moving the origin branch to a newer commit, but hasn't rebased yet.
6270 upstream_commit = None
6271 upstream_branch = opts.upstream
6272 if not upstream_branch:
6273 cl = Changelist()
6274 upstream_branch = cl.GetUpstreamBranch()
6275 if upstream_branch:
6276 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
6277 upstream_commit = upstream_commit.strip()
digit@chromium.org29e47272013-05-17 17:01:46 +00006278
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006279 if not upstream_commit:
6280 DieWithError('Could not find base commit for this branch. '
6281 'Are you in detached state?')
digit@chromium.org29e47272013-05-17 17:01:46 +00006282
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006283 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
6284 diff_output = RunGit(changed_files_cmd)
6285 diff_files = diff_output.splitlines()
6286 # Filter out files deleted by this CL
6287 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006288
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006289 if opts.js:
6290 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11006291
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006292 clang_diff_files = []
6293 if opts.clang_format:
6294 clang_diff_files = [
6295 x for x in diff_files if MatchingFileType(x, CLANG_EXTS)
6296 ]
6297 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
6298 rust_diff_files = [x for x in diff_files if MatchingFileType(x, RUST_EXTS)]
6299 swift_diff_files = [
6300 x for x in diff_files if MatchingFileType(x, SWIFT_EXTS)
Garrett Beatyed0cc5f2020-01-06 17:26:13 +00006301 ]
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006302 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00006303
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006304 top_dir = settings.GetRoot()
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00006305
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006306 return_value = _RunClangFormatDiff(opts, clang_diff_files, top_dir,
6307 upstream_commit)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006308
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006309 if opts.use_rust_fmt:
6310 rust_fmt_return_value = _RunRustFmt(opts, rust_diff_files, top_dir,
6311 upstream_commit)
6312 if rust_fmt_return_value == 2:
6313 return_value = 2
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006314
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006315 if opts.use_swift_format:
6316 if sys.platform != 'darwin':
6317 DieWithError('swift-format is only supported on macOS.')
6318 swift_format_return_value = _RunSwiftFormat(opts, swift_diff_files,
6319 top_dir, upstream_commit)
6320 if swift_format_return_value == 2:
6321 return_value = 2
Olivier Robin0a6b5442022-04-07 07:25:04 +00006322
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006323 # Similar code to above, but using yapf on .py files rather than
6324 # clang-format on C/C++ files
6325 py_explicitly_disabled = opts.python is not None and not opts.python
6326 if python_diff_files and not py_explicitly_disabled:
6327 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
6328 yapf_tool = os.path.join(depot_tools_path, 'yapf')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006329
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006330 # Used for caching.
6331 yapf_configs = {}
6332 for f in python_diff_files:
6333 # Find the yapf style config for the current file, defaults to depot
6334 # tools default.
6335 _FindYapfConfigFile(f, yapf_configs, top_dir)
Aiden Benner99b0ccb2018-11-20 19:53:31 +00006336
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006337 # Turn on python formatting by default if a yapf config is specified.
6338 # This breaks in the case of this repo though since the specified
6339 # style file is also the global default.
6340 if opts.python is None:
6341 filtered_py_files = []
6342 for f in python_diff_files:
6343 if _FindYapfConfigFile(f, yapf_configs, top_dir) is not None:
6344 filtered_py_files.append(f)
Andrew Grieveb9e694c2021-11-15 19:04:46 +00006345 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006346 filtered_py_files = python_diff_files
Peter Wend9399922020-06-17 17:33:49 +00006347
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006348 # Note: yapf still seems to fix indentation of the entire file
6349 # even if line ranges are specified.
6350 # See https://github.com/google/yapf/issues/499
6351 if not opts.full and filtered_py_files:
6352 py_line_diffs = _ComputeDiffLineRanges(filtered_py_files,
6353 upstream_commit)
Aiden Bennerc08566e2018-10-03 17:52:42 +00006354
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006355 yapfignore_patterns = _GetYapfIgnorePatterns(top_dir)
6356 filtered_py_files = _FilterYapfIgnoredFiles(filtered_py_files,
6357 yapfignore_patterns)
Aiden Bennerc08566e2018-10-03 17:52:42 +00006358
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006359 for f in filtered_py_files:
6360 yapf_style = _FindYapfConfigFile(f, yapf_configs, top_dir)
6361 # Default to pep8 if not .style.yapf is found.
6362 if not yapf_style:
6363 yapf_style = 'pep8'
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006364
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006365 with open(f, 'r') as py_f:
6366 if 'python2' in py_f.readline():
6367 vpython_script = 'vpython'
6368 else:
6369 vpython_script = 'vpython3'
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006370
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006371 cmd = [vpython_script, yapf_tool, '--style', yapf_style, f]
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006372
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006373 has_formattable_lines = False
6374 if not opts.full:
6375 # Only run yapf over changed line ranges.
6376 for diff_start, diff_len in py_line_diffs[f]:
6377 diff_end = diff_start + diff_len - 1
6378 # Yapf errors out if diff_end < diff_start but this
6379 # is a valid line range diff for a removal.
6380 if diff_end >= diff_start:
6381 has_formattable_lines = True
6382 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
6383 # If all line diffs were removals we have nothing to format.
6384 if not has_formattable_lines:
6385 continue
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006386
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006387 if opts.diff or opts.dry_run:
6388 cmd += ['--diff']
6389 # Will return non-zero exit code if non-empty diff.
6390 stdout = RunCommand(cmd,
6391 error_ok=True,
6392 stderr=subprocess2.PIPE,
6393 cwd=top_dir,
6394 shell=sys.platform.startswith('win32'))
6395 if opts.diff:
6396 sys.stdout.write(stdout)
6397 elif len(stdout) > 0:
6398 return_value = 2
6399 else:
6400 cmd += ['-i']
6401 RunCommand(cmd,
6402 cwd=top_dir,
6403 shell=sys.platform.startswith('win32'))
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006404
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006405 # Format GN build files. Always run on full build files for canonical form.
6406 if gn_diff_files:
6407 cmd = ['gn', 'format']
6408 if opts.dry_run or opts.diff:
6409 cmd.append('--dry-run')
6410 for gn_diff_file in gn_diff_files:
6411 gn_ret = subprocess2.call(cmd + [gn_diff_file],
6412 shell=sys.platform.startswith('win'),
6413 cwd=top_dir)
6414 if opts.dry_run and gn_ret == 2:
6415 return_value = 2 # Not formatted.
6416 elif opts.diff and gn_ret == 2:
6417 # TODO this should compute and print the actual diff.
6418 print('This change has GN build file diff for ' + gn_diff_file)
6419 elif gn_ret != 0:
6420 # For non-dry run cases (and non-2 return values for dry-run), a
6421 # nonzero error code indicates a failure, probably because the
6422 # file doesn't parse.
6423 DieWithError('gn format failed on ' + gn_diff_file +
6424 '\nTry running `gn format` on this file manually.')
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006425
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006426 # Skip the metrics formatting from the global presubmit hook. These files
6427 # have a separate presubmit hook that issues an error if the files need
6428 # formatting, whereas the top-level presubmit script merely issues a
6429 # warning. Formatting these files is somewhat slow, so it's important not to
6430 # duplicate the work.
6431 if not opts.presubmit:
6432 for diff_xml in GetDiffXMLs(diff_files):
6433 xml_dir = GetMetricsDir(diff_xml)
6434 if not xml_dir:
6435 continue
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006436
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006437 tool_dir = os.path.join(top_dir, xml_dir)
6438 pretty_print_tool = os.path.join(tool_dir, 'pretty_print.py')
6439 cmd = ['vpython3', pretty_print_tool, '--non-interactive']
Alexei Svitkinef3cac412017-02-06 11:08:50 -05006440
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006441 # If the XML file is histograms.xml or enums.xml, add the xml path
6442 # to the command as histograms/pretty_print.py now needs a relative
6443 # path argument after splitting the histograms into multiple
6444 # directories. For example, in tools/metrics/ukm, pretty-print could
6445 # be run using: $ python pretty_print.py But in
6446 # tools/metrics/histogrmas, pretty-print should be run with an
6447 # additional relative path argument, like: $ python pretty_print.py
6448 # metadata/UMA/histograms.xml $ python pretty_print.py enums.xml
6449
6450 if xml_dir == os.path.join('tools', 'metrics', 'histograms'):
6451 if os.path.basename(diff_xml) not in (
6452 'histograms.xml', 'enums.xml',
6453 'histogram_suffixes_list.xml'):
6454 # Skip this XML file if it's not one of the known types.
6455 continue
6456 cmd.append(diff_xml)
6457
6458 if opts.dry_run or opts.diff:
6459 cmd.append('--diff')
6460
6461 # TODO(isherman): Once this file runs only on Python 3.3+, drop the
6462 # `shell` param and instead replace `'vpython'` with
6463 # `shutil.which('frob')` above: https://stackoverflow.com/a/32799942
6464 stdout = RunCommand(cmd,
6465 cwd=top_dir,
6466 shell=sys.platform.startswith('win32'))
6467 if opts.diff:
6468 sys.stdout.write(stdout)
6469 if opts.dry_run and stdout:
6470 return_value = 2 # Not formatted.
6471
6472 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006473
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00006474
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006475def GetDiffXMLs(diff_files):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006476 return [
6477 os.path.normpath(x) for x in diff_files
6478 if MatchingFileType(x, ['.xml'])
6479 ]
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006480
6481
6482def GetMetricsDir(diff_xml):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006483 metrics_xml_dirs = [
6484 os.path.join('tools', 'metrics', 'actions'),
6485 os.path.join('tools', 'metrics', 'histograms'),
6486 os.path.join('tools', 'metrics', 'structured'),
6487 os.path.join('tools', 'metrics', 'ukm'),
6488 ]
6489 for xml_dir in metrics_xml_dirs:
6490 if diff_xml.startswith(xml_dir):
6491 return xml_dir
6492 return None
Steven Holte2e664bf2017-04-21 13:10:47 -07006493
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006494
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006495@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006496@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006497def CMDcheckout(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006498 """Checks out a branch associated with a given Gerrit issue."""
6499 _, args = parser.parse_args(args)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006500
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006501 if len(args) != 1:
6502 parser.print_help()
6503 return 1
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006504
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006505 issue_arg = ParseIssueNumberArgument(args[0])
6506 if not issue_arg.valid:
6507 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02006508
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006509 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006510
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006511 output = RunGit([
6512 'config', '--local', '--get-regexp', r'branch\..*\.' + ISSUE_CONFIG_KEY
6513 ],
6514 error_ok=True)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006515
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006516 branches = []
6517 for key, issue in [x.split() for x in output.splitlines()]:
6518 if issue == target_issue:
6519 branches.append(
6520 re.sub(r'branch\.(.*)\.' + ISSUE_CONFIG_KEY, r'\1', key))
Edward Lemur52969c92020-02-06 18:15:28 +00006521
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006522 if len(branches) == 0:
6523 print('No branch found for issue %s.' % target_issue)
6524 return 1
6525 if len(branches) == 1:
6526 RunGit(['checkout', branches[0]])
6527 else:
6528 print('Multiple branches match issue %s:' % target_issue)
6529 for i in range(len(branches)):
6530 print('%d: %s' % (i, branches[i]))
6531 which = gclient_utils.AskForData('Choose by index: ')
6532 try:
6533 RunGit(['checkout', branches[int(which)]])
6534 except (IndexError, ValueError):
6535 print('Invalid selection, not checking out any branch.')
6536 return 1
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006537
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006538 return 0
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006539
6540
maruel@chromium.org29404b52014-09-08 22:58:00 +00006541def CMDlol(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006542 # This command is intentionally undocumented.
6543 print(
6544 zlib.decompress(
6545 base64.b64decode(
6546 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
6547 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
6548 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
6549 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')).decode('utf-8'))
6550 return 0
maruel@chromium.org29404b52014-09-08 22:58:00 +00006551
6552
Josip Sokcevic0399e172022-03-21 23:11:51 +00006553def CMDversion(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006554 import utils
6555 print(utils.depot_tools_version())
Josip Sokcevic0399e172022-03-21 23:11:51 +00006556
6557
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006558class OptionParser(optparse.OptionParser):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006559 """Creates the option parse and add --verbose support."""
6560 def __init__(self, *args, **kwargs):
6561 optparse.OptionParser.__init__(self,
6562 *args,
6563 prog='git cl',
6564 version=__version__,
6565 **kwargs)
6566 self.add_option('-v',
6567 '--verbose',
6568 action='count',
6569 default=0,
6570 help='Use 2 times for more debugging info')
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00006571
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006572 def parse_args(self, args=None, _values=None):
Joanna Wangc5b38322023-03-15 20:38:46 +00006573 try:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006574 return self._parse_args(args)
6575 finally:
6576 # Regardless of success or failure of args parsing, we want to
6577 # report metrics, but only after logging has been initialized (if
6578 # parsing succeeded).
6579 global settings
6580 settings = Settings()
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00006581
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006582 if metrics.collector.config.should_collect_metrics:
6583 try:
6584 # GetViewVCUrl ultimately calls logging method.
6585 project_url = settings.GetViewVCUrl().strip('/+')
6586 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
6587 metrics.collector.add('project_urls', [project_url])
6588 except subprocess2.CalledProcessError:
6589 # Occurs when command is not executed in a git repository
6590 # We should not fail here. If the command needs to be
6591 # executed in a repo, it will be raised later.
6592 pass
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006593
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006594 def _parse_args(self, args=None):
6595 # Create an optparse.Values object that will store only the actual
6596 # passed options, without the defaults.
6597 actual_options = optparse.Values()
6598 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
6599 # Create an optparse.Values object with the default options.
6600 options = optparse.Values(self.get_default_values().__dict__)
6601 # Update it with the options passed by the user.
6602 options._update_careful(actual_options.__dict__)
6603 # Store the options passed by the user in an _actual_options attribute.
6604 # We store only the keys, and not the values, since the values can
6605 # contain arbitrary information, which might be PII.
6606 metrics.collector.add('arguments', list(actual_options.__dict__.keys()))
Edward Lemur83bd7f42018-10-10 00:14:21 +00006607
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006608 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
6609 logging.basicConfig(
6610 level=levels[min(options.verbose,
6611 len(levels) - 1)],
6612 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
6613 '%(filename)s] %(message)s')
6614
6615 return options, args
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006616
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006617
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006618def main(argv):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006619 if sys.hexversion < 0x02060000:
6620 print('\nYour Python version %s is unsupported, please upgrade.\n' %
6621 (sys.version.split(' ', 1)[0], ),
6622 file=sys.stderr)
6623 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006624
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006625 colorize_CMDstatus_doc()
6626 dispatcher = subcommand.CommandDispatcher(__name__)
6627 try:
6628 return dispatcher.execute(OptionParser(), argv)
6629 except auth.LoginRequiredError as e:
6630 DieWithError(str(e))
6631 except urllib.error.HTTPError as e:
6632 if e.code != 500:
6633 raise
6634 DieWithError((
6635 'App Engine is misbehaving and returned HTTP %d, again. Keep faith '
6636 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
6637 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006638
6639
6640if __name__ == '__main__':
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006641 # These affect sys.stdout, so do it outside of main() to simplify mocks in
6642 # the unit tests.
6643 fix_encoding.fix_encoding()
6644 setup_color.init()
6645 with metrics.collector.print_notice_and_exit():
6646 sys.exit(main(sys.argv[1:]))