blob: cfb62348e87aedb75100a3f030b87533769221e6 [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
Gavin Mak7f5b53f2023-09-07 18:13:01 +000027import urllib.parse
28import urllib.request
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000029import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000030import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000031import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000032
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000033from third_party import colorama
Daniel Chengabf48472023-08-30 15:45:13 +000034from typing import Any
35from typing import List
36from typing import Mapping
37from typing import NoReturn
Daniel Cheng66d0f152023-08-29 23:21:58 +000038from typing import Optional
39from typing import Sequence
40from typing import Tuple
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000041import auth
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000042import clang_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000043import fix_encoding
Andrew Grieved7ba85d2023-09-15 18:28:33 +000044import gclient_paths
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000045import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000046import gerrit_util
iannucci@chromium.org9e849272014-04-04 00:31:55 +000047import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000048import git_footers
Edward Lemur85153282020-02-14 22:06:29 +000049import git_new_branch
Edward Lemur5ba1e9c2018-07-23 18:19:02 +000050import metrics
Edward Lesmes93277a72018-10-18 22:04:26 +000051import metrics_utils
Edward Lesmeseeca9c62020-11-20 00:00:17 +000052import owners_client
iannucci@chromium.org9e849272014-04-04 00:31:55 +000053import owners_finder
Lei Zhangb8c62cf2020-07-15 20:09:37 +000054import presubmit_canned_checks
Josip Sokcevic7958e302023-03-01 23:02:21 +000055import presubmit_support
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +000056import rustfmt
Josip Sokcevic7958e302023-03-01 23:02:21 +000057import scm
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +000058import setup_color
Francois Dorayd42c6812017-05-30 15:10:20 -040059import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000060import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000061import subprocess2
Olivier Robin0a6b5442022-04-07 07:25:04 +000062import swift_format
maruel@chromium.org2a74d372011-03-29 19:05:50 +000063import watchlists
64
Edward Lemur79d4f992019-11-11 23:49:02 +000065
tandrii7400cf02016-06-21 08:48:07 -070066__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000067
Mike Frysinger124bb8e2023-09-06 05:48:55 +000068# TODO: Should fix these warnings.
69# pylint: disable=line-too-long
70
Edward Lemur0f58ae42019-04-30 17:24:12 +000071# Traces for git push will be stored in a traces directory inside the
72# depot_tools checkout.
73DEPOT_TOOLS = os.path.dirname(os.path.abspath(__file__))
74TRACES_DIR = os.path.join(DEPOT_TOOLS, 'traces')
Edward Lemur227d5102020-02-25 23:45:35 +000075PRESUBMIT_SUPPORT = os.path.join(DEPOT_TOOLS, 'presubmit_support.py')
Edward Lemur0f58ae42019-04-30 17:24:12 +000076
77# When collecting traces, Git hashes will be reduced to 6 characters to reduce
78# the size after compression.
79GIT_HASH_RE = re.compile(r'\b([a-f0-9]{6})[a-f0-9]{34}\b', flags=re.I)
80# Used to redact the cookies from the gitcookies file.
81GITCOOKIES_REDACT_RE = re.compile(r'1/.*')
82
Edward Lemurd4d1ba42019-09-20 21:46:37 +000083MAX_ATTEMPTS = 3
84
Edward Lemur1b52d872019-05-09 21:12:12 +000085# The maximum number of traces we will keep. Multiplied by 3 since we store
86# 3 files per trace.
87MAX_TRACES = 3 * 10
Edward Lemur5737f022019-05-17 01:24:00 +000088# Message to be displayed to the user to inform where to find the traces for a
89# git-cl upload execution.
Edward Lemur0f58ae42019-04-30 17:24:12 +000090TRACES_MESSAGE = (
Mike Frysinger124bb8e2023-09-06 05:48:55 +000091 '\n'
92 'The traces of this git-cl execution have been recorded at:\n'
93 ' %(trace_name)s-traces.zip\n'
94 'Copies of your gitcookies file and git config have been recorded at:\n'
95 ' %(trace_name)s-git-info.zip\n')
Edward Lemur1b52d872019-05-09 21:12:12 +000096# Format of the message to be stored as part of the traces to give developers a
97# better context when they go through traces.
Mike Frysinger124bb8e2023-09-06 05:48:55 +000098TRACES_README_FORMAT = ('Date: %(now)s\n'
99 '\n'
100 'Change: https://%(gerrit_host)s/q/%(change_id)s\n'
101 'Title: %(title)s\n'
102 '\n'
103 '%(description)s\n'
104 '\n'
105 'Execution time: %(execution_time)s\n'
106 'Exit code: %(exit_code)s\n') + TRACES_MESSAGE
Edward Lemur0f58ae42019-04-30 17:24:12 +0000107
Aaron Gable1bc7bfe2016-12-19 10:08:14 -0800108POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
Henrique Ferreiroff249622019-11-28 23:19:29 +0000109DESCRIPTION_BACKUP_FILE = '.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +0000110REFS_THAT_ALIAS_TO_OTHER_REFS = {
Josip Sokcevic7e133ff2021-07-13 17:44:53 +0000111 'refs/remotes/origin/lkgr': 'refs/remotes/origin/main',
112 'refs/remotes/origin/lkcr': 'refs/remotes/origin/main',
rmistry@google.comc68112d2015-03-03 12:48:06 +0000113}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000114
Josip Sokcevicc39ab992020-09-24 20:09:15 +0000115DEFAULT_OLD_BRANCH = 'refs/remotes/origin/master'
116DEFAULT_NEW_BRANCH = 'refs/remotes/origin/main'
117
Joanna Wanga8db0cb2023-01-24 15:43:17 +0000118DEFAULT_BUILDBUCKET_HOST = 'cr-buildbucket.appspot.com'
119
thestig@chromium.org44202a22014-03-11 19:22:18 +0000120# Valid extensions for files we want to lint.
121DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
122DEFAULT_LINT_IGNORE_REGEX = r"$^"
123
Aiden Bennerc08566e2018-10-03 17:52:42 +0000124# File name for yapf style config files.
125YAPF_CONFIG_FILENAME = '.style.yapf'
126
Edward Lesmes50da7702020-03-30 19:23:43 +0000127# The issue, patchset and codereview server are stored on git config for each
128# branch under branch.<branch-name>.<config-key>.
129ISSUE_CONFIG_KEY = 'gerritissue'
130PATCHSET_CONFIG_KEY = 'gerritpatchset'
131CODEREVIEW_SERVER_CONFIG_KEY = 'gerritserver'
Gavin Makbe2e9262022-11-08 23:41:55 +0000132# When using squash workflow, _CMDUploadChange doesn't simply push the commit(s)
133# you make to Gerrit. Instead, it creates a new commit object that contains all
134# changes you've made, diffed against a parent/merge base.
135# This is the hash of the new squashed commit and you can find this on Gerrit.
136GERRIT_SQUASH_HASH_CONFIG_KEY = 'gerritsquashhash'
137# This is the latest uploaded local commit hash.
138LAST_UPLOAD_HASH_CONFIG_KEY = 'last-upload-hash'
Edward Lesmes50da7702020-03-30 19:23:43 +0000139
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000140# Shortcut since it quickly becomes repetitive.
maruel@chromium.org2e23ce32013-05-07 12:42:28 +0000141Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +0000142
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000143# Initialized in main()
144settings = None
145
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100146# Used by tests/git_cl_test.py to add extra logging.
147# Inside the weirdly failing test, add this:
148# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700149# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100150_IS_BEING_TESTED = False
151
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000152_GOOGLESOURCE = 'googlesource.com'
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000153
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +0000154_KNOWN_GERRIT_TO_SHORT_URLS = {
155 'https://chrome-internal-review.googlesource.com': 'https://crrev.com/i',
156 'https://chromium-review.googlesource.com': 'https://crrev.com/c',
157}
Andrii Shyshkalov8aebb602020-04-16 22:10:27 +0000158assert len(_KNOWN_GERRIT_TO_SHORT_URLS) == len(
159 set(_KNOWN_GERRIT_TO_SHORT_URLS.values())), 'must have unique values'
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +0000160
Joanna Wang18de1f62023-01-21 01:24:24 +0000161# Maximum number of branches in a stack that can be traversed and uploaded
162# at once. Picked arbitrarily.
163_MAX_STACKED_BRANCHES_UPLOAD = 20
164
Joanna Wang892f2ce2023-03-14 21:39:47 +0000165# Environment variable to indicate if user is participating in the stcked
166# changes dogfood.
167DOGFOOD_STACKED_CHANGES_VAR = 'DOGFOOD_STACKED_CHANGES'
168
169
Josip Sokcevicf736cab2020-10-20 23:41:38 +0000170class GitPushError(Exception):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000171 pass
Josip Sokcevicf736cab2020-10-20 23:41:38 +0000172
173
Daniel Chengabf48472023-08-30 15:45:13 +0000174def DieWithError(message, change_desc=None) -> NoReturn:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000175 if change_desc:
176 SaveDescriptionBackup(change_desc)
177 print('\n ** Content of CL description **\n' + '=' * 72 + '\n' +
178 change_desc.description + '\n' + '=' * 72 + '\n')
Christopher Lamf732cd52017-01-24 12:40:11 +1100179
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000180 print(message, file=sys.stderr)
181 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000182
183
Christopher Lamf732cd52017-01-24 12:40:11 +1100184def SaveDescriptionBackup(change_desc):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000185 backup_path = os.path.join(DEPOT_TOOLS, DESCRIPTION_BACKUP_FILE)
186 print('\nsaving CL description to %s\n' % backup_path)
187 with open(backup_path, 'wb') as backup_file:
188 backup_file.write(change_desc.description.encode('utf-8'))
Christopher Lamf732cd52017-01-24 12:40:11 +1100189
190
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000191def GetNoGitPagerEnv():
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000192 env = os.environ.copy()
193 # 'cat' is a magical git string that disables pagers on all platforms.
194 env['GIT_PAGER'] = 'cat'
195 return env
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000196
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000197
bsep@chromium.org627d9002016-04-29 00:00:52 +0000198def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000199 try:
200 stdout = subprocess2.check_output(args, shell=shell, **kwargs)
201 return stdout.decode('utf-8', 'replace')
202 except subprocess2.CalledProcessError as e:
203 logging.debug('Failed running %s', args)
204 if not error_ok:
205 message = error_message or e.stdout.decode('utf-8', 'replace') or ''
206 DieWithError('Command "%s" failed.\n%s' % (' '.join(args), message))
207 out = e.stdout.decode('utf-8', 'replace')
208 if e.stderr:
209 out += e.stderr.decode('utf-8', 'replace')
210 return out
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000211
212
213def RunGit(args, **kwargs):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000214 """Returns stdout."""
215 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000216
217
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000218def RunGitWithCode(args, suppress_stderr=False):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000219 """Returns return code and stdout."""
220 if suppress_stderr:
221 stderr = subprocess2.DEVNULL
222 else:
223 stderr = sys.stderr
224 try:
225 (out, _), code = subprocess2.communicate(['git'] + args,
226 env=GetNoGitPagerEnv(),
227 stdout=subprocess2.PIPE,
228 stderr=stderr)
229 return code, out.decode('utf-8', 'replace')
230 except subprocess2.CalledProcessError as e:
231 logging.debug('Failed running %s', ['git'] + args)
232 return e.returncode, e.stdout.decode('utf-8', 'replace')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000233
234
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000235def RunGitSilent(args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000236 """Returns stdout, suppresses stderr and ignores the return code."""
237 return RunGitWithCode(args, suppress_stderr=True)[1]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000238
239
tandrii2a16b952016-10-19 07:09:44 -0700240def time_sleep(seconds):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000241 # Use this so that it can be mocked in tests without interfering with python
242 # system machinery.
243 return time.sleep(seconds)
tandrii2a16b952016-10-19 07:09:44 -0700244
245
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000246def time_time():
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000247 # Use this so that it can be mocked in tests without interfering with python
248 # system machinery.
249 return time.time()
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000250
251
Edward Lemur1b52d872019-05-09 21:12:12 +0000252def datetime_now():
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000253 # Use this so that it can be mocked in tests without interfering with python
254 # system machinery.
255 return datetime.datetime.now()
Edward Lemur1b52d872019-05-09 21:12:12 +0000256
257
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100258def confirm_or_exit(prefix='', action='confirm'):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000259 """Asks user to press enter to continue or press Ctrl+C to abort."""
260 if not prefix or prefix.endswith('\n'):
261 mid = 'Press'
262 elif prefix.endswith('.') or prefix.endswith('?'):
263 mid = ' Press'
264 elif prefix.endswith(' '):
265 mid = 'press'
266 else:
267 mid = ' press'
268 gclient_utils.AskForData('%s%s Enter to %s, or Ctrl+C to abort' %
269 (prefix, mid, action))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100270
271
272def ask_for_explicit_yes(prompt):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000273 """Returns whether user typed 'y' or 'yes' to confirm the given prompt."""
274 result = gclient_utils.AskForData(prompt + ' [Yes/No]: ').lower()
275 while True:
276 if 'yes'.startswith(result):
277 return True
278 if 'no'.startswith(result):
279 return False
280 result = gclient_utils.AskForData('Please, type yes or no: ').lower()
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100281
282
machenbach@chromium.org45453142015-09-15 08:45:22 +0000283def _get_properties_from_options(options):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000284 prop_list = getattr(options, 'properties', [])
285 properties = dict(x.split('=', 1) for x in prop_list)
286 for key, val in properties.items():
287 try:
288 properties[key] = json.loads(val)
289 except ValueError:
290 pass # If a value couldn't be evaluated, treat it as a string.
291 return properties
machenbach@chromium.org45453142015-09-15 08:45:22 +0000292
293
Edward Lemur4c707a22019-09-24 21:13:43 +0000294def _call_buildbucket(http, buildbucket_host, method, request):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000295 """Calls a buildbucket v2 method and returns the parsed json response."""
296 headers = {
297 'Accept': 'application/json',
298 'Content-Type': 'application/json',
299 }
300 request = json.dumps(request)
301 url = 'https://%s/prpc/buildbucket.v2.Builds/%s' % (buildbucket_host,
302 method)
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000303
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000304 logging.info('POST %s with %s' % (url, request))
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000305
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000306 attempts = 1
307 time_to_sleep = 1
308 while True:
309 response, content = http.request(url,
310 'POST',
311 body=request,
312 headers=headers)
313 if response.status == 200:
314 return json.loads(content[4:])
315 if attempts >= MAX_ATTEMPTS or 400 <= response.status < 500:
316 msg = '%s error when calling POST %s with %s: %s' % (
317 response.status, url, request, content)
318 raise BuildbucketResponseException(msg)
319 logging.debug('%s error when calling POST %s with %s. '
320 'Sleeping for %d seconds and retrying...' %
321 (response.status, url, request, time_to_sleep))
322 time.sleep(time_to_sleep)
323 time_to_sleep *= 2
324 attempts += 1
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000325
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000326 assert False, 'unreachable'
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000327
328
Edward Lemur6215c792019-10-03 21:59:05 +0000329def _parse_bucket(raw_bucket):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000330 legacy = True
331 project = bucket = None
332 if '/' in raw_bucket:
333 legacy = False
334 project, bucket = raw_bucket.split('/', 1)
335 # Assume luci.<project>.<bucket>.
336 elif raw_bucket.startswith('luci.'):
337 project, bucket = raw_bucket[len('luci.'):].split('.', 1)
338 # Otherwise, assume prefix is also the project name.
339 elif '.' in raw_bucket:
340 project = raw_bucket.split('.')[0]
341 bucket = raw_bucket
342 # Legacy buckets.
343 if legacy and project and bucket:
344 print('WARNING Please use %s/%s to specify the bucket.' %
345 (project, bucket))
346 return project, bucket
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000347
348
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000349def _canonical_git_googlesource_host(host):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000350 """Normalizes Gerrit hosts (with '-review') to Git host."""
351 assert host.endswith(_GOOGLESOURCE)
352 # Prefix doesn't include '.' at the end.
353 prefix = host[:-(1 + len(_GOOGLESOURCE))]
354 if prefix.endswith('-review'):
355 prefix = prefix[:-len('-review')]
356 return prefix + '.' + _GOOGLESOURCE
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000357
358
359def _canonical_gerrit_googlesource_host(host):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000360 git_host = _canonical_git_googlesource_host(host)
361 prefix = git_host.split('.', 1)[0]
362 return prefix + '-review.' + _GOOGLESOURCE
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000363
364
365def _get_counterpart_host(host):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000366 assert host.endswith(_GOOGLESOURCE)
367 git = _canonical_git_googlesource_host(host)
368 gerrit = _canonical_gerrit_googlesource_host(git)
369 return git if gerrit == host else gerrit
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000370
371
Quinten Yearsley777660f2020-03-04 23:37:06 +0000372def _trigger_tryjobs(changelist, jobs, options, patchset):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000373 """Sends a request to Buildbucket to trigger tryjobs for a changelist.
qyearsley1fdfcb62016-10-24 13:22:03 -0700374
375 Args:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000376 changelist: Changelist that the tryjobs are associated with.
Edward Lemur45768512020-03-02 19:03:14 +0000377 jobs: A list of (project, bucket, builder).
qyearsley1fdfcb62016-10-24 13:22:03 -0700378 options: Command-line options.
379 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000380 print('Scheduling jobs on:')
381 for project, bucket, builder in jobs:
382 print(' %s/%s: %s' % (project, bucket, builder))
383 print('To see results here, run: git cl try-results')
384 print('To see results in browser, run: git cl web')
tandriide281ae2016-10-12 06:02:30 -0700385
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000386 requests = _make_tryjob_schedule_requests(changelist, jobs, options,
387 patchset)
388 if not requests:
389 return
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000390
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000391 http = auth.Authenticator().authorize(httplib2.Http())
392 http.force_exception_to_status_code = True
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000393
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000394 batch_request = {'requests': requests}
395 batch_response = _call_buildbucket(http, DEFAULT_BUILDBUCKET_HOST, 'Batch',
396 batch_request)
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000397
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000398 errors = [
399 ' ' + response['error']['message']
400 for response in batch_response.get('responses', [])
401 if 'error' in response
402 ]
403 if errors:
404 raise BuildbucketResponseException(
405 'Failed to schedule builds for some bots:\n%s' % '\n'.join(errors))
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000406
407
Quinten Yearsley777660f2020-03-04 23:37:06 +0000408def _make_tryjob_schedule_requests(changelist, jobs, options, patchset):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000409 """Constructs requests for Buildbucket to trigger tryjobs."""
410 gerrit_changes = [changelist.GetGerritChange(patchset)]
411 shared_properties = {
412 'category': options.ensure_value('category', 'git_cl_try')
413 }
414 if options.ensure_value('clobber', False):
415 shared_properties['clobber'] = True
416 shared_properties.update(_get_properties_from_options(options) or {})
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000417
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000418 shared_tags = [{'key': 'user_agent', 'value': 'git_cl_try'}]
419 if options.ensure_value('retry_failed', False):
420 shared_tags.append({'key': 'retry_failed', 'value': '1'})
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000421
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000422 requests = []
423 for (project, bucket, builder) in jobs:
424 properties = shared_properties.copy()
425 if 'presubmit' in builder.lower():
426 properties['dry_run'] = 'true'
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000427
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000428 requests.append({
429 'scheduleBuild': {
430 'requestId': str(uuid.uuid4()),
431 'builder': {
432 'project': getattr(options, 'project', None) or project,
433 'bucket': bucket,
434 'builder': builder,
435 },
436 'gerritChanges': gerrit_changes,
437 'properties': properties,
438 'tags': [
439 {
440 'key': 'builder',
441 'value': builder
442 },
443 ] + shared_tags,
444 }
445 })
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000446
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000447 if options.ensure_value('revision', None):
448 remote, remote_branch = changelist.GetRemoteBranch()
449 requests[-1]['scheduleBuild']['gitilesCommit'] = {
450 'host':
451 _canonical_git_googlesource_host(gerrit_changes[0]['host']),
452 'project': gerrit_changes[0]['project'],
453 'id': options.revision,
454 'ref': GetTargetRef(remote, remote_branch, None)
455 }
Anthony Polito1a5fe232020-01-24 23:17:52 +0000456
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000457 return requests
kjellander@chromium.org44424542015-06-02 18:35:29 +0000458
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000459
Quinten Yearsley777660f2020-03-04 23:37:06 +0000460def _fetch_tryjobs(changelist, buildbucket_host, patchset=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000461 """Fetches tryjobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000462
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000463 Returns list of buildbucket.v2.Build with the try jobs for the changelist.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000464 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000465 fields = ['id', 'builder', 'status', 'createTime', 'tags']
466 request = {
467 'predicate': {
468 'gerritChanges': [changelist.GetGerritChange(patchset)],
469 },
470 'fields': ','.join('builds.*.' + field for field in fields),
471 }
tandrii221ab252016-10-06 08:12:04 -0700472
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000473 authenticator = auth.Authenticator()
474 if authenticator.has_cached_credentials():
475 http = authenticator.authorize(httplib2.Http())
476 else:
477 print('Warning: Some results might be missing because %s' %
478 # Get the message on how to login.
479 (
480 str(auth.LoginRequiredError()), ))
481 http = httplib2.Http()
482 http.force_exception_to_status_code = True
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000483
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000484 response = _call_buildbucket(http, buildbucket_host, 'SearchBuilds',
485 request)
486 return response.get('builds', [])
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000487
Edward Lemur45768512020-03-02 19:03:14 +0000488
Edward Lemur5b929a42019-10-21 17:57:39 +0000489def _fetch_latest_builds(changelist, buildbucket_host, latest_patchset=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000490 """Fetches builds from the latest patchset that has builds (within
Quinten Yearsley983111f2019-09-26 17:18:48 +0000491 the last few patchsets).
492
493 Args:
Quinten Yearsley983111f2019-09-26 17:18:48 +0000494 changelist (Changelist): The CL to fetch builds for
495 buildbucket_host (str): Buildbucket host, e.g. "cr-buildbucket.appspot.com"
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +0000496 lastest_patchset(int|NoneType): the patchset to start fetching builds from.
497 If None (default), starts with the latest available patchset.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000498 Returns:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000499 A tuple (builds, patchset) where builds is a list of buildbucket.v2.Build,
500 and patchset is the patchset number where those builds came from.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000501 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000502 assert buildbucket_host
503 assert changelist.GetIssue(), 'CL must be uploaded first'
504 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
505 if latest_patchset is None:
506 assert changelist.GetMostRecentPatchset()
507 ps = changelist.GetMostRecentPatchset()
508 else:
509 assert latest_patchset > 0, latest_patchset
510 ps = latest_patchset
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +0000511
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000512 min_ps = max(1, ps - 5)
513 while ps >= min_ps:
514 builds = _fetch_tryjobs(changelist, buildbucket_host, patchset=ps)
515 if len(builds):
516 return builds, ps
517 ps -= 1
518 return [], 0
Quinten Yearsley983111f2019-09-26 17:18:48 +0000519
520
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000521def _filter_failed_for_retry(all_builds):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000522 """Returns a list of buckets/builders that are worth retrying.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000523
524 Args:
Quinten Yearsley777660f2020-03-04 23:37:06 +0000525 all_builds (list): Builds, in the format returned by _fetch_tryjobs,
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000526 i.e. a list of buildbucket.v2.Builds which includes status and builder
527 info.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000528
529 Returns:
Edward Lemur45768512020-03-02 19:03:14 +0000530 A dict {(proj, bucket): [builders]}. This is the same format accepted by
Quinten Yearsley777660f2020-03-04 23:37:06 +0000531 _trigger_tryjobs.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000532 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000533 grouped = {}
534 for build in all_builds:
535 builder = build['builder']
536 key = (builder['project'], builder['bucket'], builder['builder'])
537 grouped.setdefault(key, []).append(build)
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000538
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000539 jobs = []
540 for (project, bucket, builder), builds in grouped.items():
541 if 'triggered' in builder:
542 print(
543 'WARNING: Not scheduling %s. Triggered bots require an initial job '
544 'from a parent. Please schedule a manual job for the parent '
545 'instead.')
546 continue
547 if any(b['status'] in ('STARTED', 'SCHEDULED') for b in builds):
548 # Don't retry if any are running.
549 continue
550 # If builder had several builds, retry only if the last one failed.
551 # This is a bit different from CQ, which would re-use *any* SUCCESS-full
552 # build, but in case of retrying failed jobs retrying a flaky one makes
553 # sense.
554 builds = sorted(builds, key=lambda b: b['createTime'])
555 if builds[-1]['status'] not in ('FAILURE', 'INFRA_FAILURE'):
556 continue
557 # Don't retry experimental build previously triggered by CQ.
558 if any(t['key'] == 'cq_experimental' and t['value'] == 'true'
559 for t in builds[-1]['tags']):
560 continue
561 jobs.append((project, bucket, builder))
Edward Lemur45768512020-03-02 19:03:14 +0000562
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000563 # Sort the jobs to make testing easier.
564 return sorted(jobs)
Quinten Yearsley983111f2019-09-26 17:18:48 +0000565
566
Quinten Yearsley777660f2020-03-04 23:37:06 +0000567def _print_tryjobs(options, builds):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000568 """Prints nicely result of _fetch_tryjobs."""
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000569 if not builds:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000570 print('No tryjobs scheduled.')
571 return
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000572
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000573 longest_builder = max(len(b['builder']['builder']) for b in builds)
574 name_fmt = '{builder:<%d}' % longest_builder
575 if options.print_master:
576 longest_bucket = max(len(b['builder']['bucket']) for b in builds)
577 name_fmt = ('{bucket:>%d} ' % longest_bucket) + name_fmt
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000578
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000579 builds_by_status = {}
580 for b in builds:
581 builds_by_status.setdefault(b['status'], []).append({
582 'id':
583 b['id'],
584 'name':
585 name_fmt.format(builder=b['builder']['builder'],
586 bucket=b['builder']['bucket']),
587 })
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000588
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000589 sort_key = lambda b: (b['name'], b['id'])
590
591 def print_builds(title, builds, fmt=None, color=None):
592 """Pop matching builds from `builds` dict and print them."""
593 if not builds:
594 return
595
596 fmt = fmt or '{name} https://ci.chromium.org/b/{id}'
597 if not options.color or color is None:
598 colorize = lambda x: x
599 else:
600 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
601
602 print(colorize(title))
603 for b in sorted(builds, key=sort_key):
604 print(' ', colorize(fmt.format(**b)))
605
606 total = len(builds)
607 print_builds('Successes:',
608 builds_by_status.pop('SUCCESS', []),
609 color=Fore.GREEN)
610 print_builds('Infra Failures:',
611 builds_by_status.pop('INFRA_FAILURE', []),
612 color=Fore.MAGENTA)
613 print_builds('Failures:',
614 builds_by_status.pop('FAILURE', []),
615 color=Fore.RED)
616 print_builds('Canceled:',
617 builds_by_status.pop('CANCELED', []),
618 fmt='{name}',
619 color=Fore.MAGENTA)
620 print_builds('Started:',
621 builds_by_status.pop('STARTED', []),
622 color=Fore.YELLOW)
623 print_builds('Scheduled:',
624 builds_by_status.pop('SCHEDULED', []),
625 fmt='{name} id={id}')
626 # The last section is just in case buildbucket API changes OR there is a
627 # bug.
628 print_builds('Other:',
629 sum(builds_by_status.values(), []),
630 fmt='{name} id={id}')
631 print('Total: %d tryjobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000632
633
Andrew Grieved7ba85d2023-09-15 18:28:33 +0000634def _ComputeFormatDiffLineRanges(files, upstream_commit):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000635 """Gets the changed line ranges for each file since upstream_commit.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000636
637 Parses a git diff on provided files and returns a dict that maps a file name
638 to an ordered list of range tuples in the form (start_line, count).
639 Ranges are in the same format as a git diff.
640 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000641 # If files is empty then diff_output will be a full diff.
642 if len(files) == 0:
643 return {}
Aiden Bennerc08566e2018-10-03 17:52:42 +0000644
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000645 # Take the git diff and find the line ranges where there are changes.
646 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
647 diff_output = RunGit(diff_cmd)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000648
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000649 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
650 # 2 capture groups
651 # 0 == fname of diff file
652 # 1 == 'diff_start,diff_count' or 'diff_start'
653 # will match each of
654 # diff --git a/foo.foo b/foo.py
655 # @@ -12,2 +14,3 @@
656 # @@ -12,2 +17 @@
657 # running re.findall on the above string with pattern will give
658 # [('foo.py', ''), ('', '14,3'), ('', '17')]
Aiden Bennerc08566e2018-10-03 17:52:42 +0000659
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000660 curr_file = None
661 line_diffs = {}
662 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
663 if match[0] != '':
664 # Will match the second filename in diff --git a/a.py b/b.py.
665 curr_file = match[0]
666 line_diffs[curr_file] = []
667 else:
668 # Matches +14,3
669 if ',' in match[1]:
670 diff_start, diff_count = match[1].split(',')
671 else:
672 # Single line changes are of the form +12 instead of +12,1.
673 diff_start = match[1]
674 diff_count = 1
Aiden Bennerc08566e2018-10-03 17:52:42 +0000675
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000676 diff_start = int(diff_start)
677 diff_count = int(diff_count)
Andrew Grieved7ba85d2023-09-15 18:28:33 +0000678 diff_end = diff_start + diff_count - 1
Aiden Bennerc08566e2018-10-03 17:52:42 +0000679
Andrew Grieved7ba85d2023-09-15 18:28:33 +0000680 # Only format added ranges (not removed ones).
681 if diff_end >= diff_start:
682 line_diffs[curr_file].append((diff_start, diff_end))
Aiden Bennerc08566e2018-10-03 17:52:42 +0000683
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000684 return line_diffs
Aiden Bennerc08566e2018-10-03 17:52:42 +0000685
686
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000687def _FindYapfConfigFile(fpath, yapf_config_cache, top_dir=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000688 """Checks if a yapf file is in any parent directory of fpath until top_dir.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000689
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000690 Recursively checks parent directories to find yapf file and if no yapf file
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000691 is found returns None. Uses yapf_config_cache as a cache for previously found
692 configs.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000693 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000694 fpath = os.path.abspath(fpath)
695 # Return result if we've already computed it.
696 if fpath in yapf_config_cache:
697 return yapf_config_cache[fpath]
Aiden Bennerc08566e2018-10-03 17:52:42 +0000698
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000699 parent_dir = os.path.dirname(fpath)
700 if os.path.isfile(fpath):
701 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000702 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000703 # Otherwise fpath is a directory
704 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
705 if os.path.isfile(yapf_file):
706 ret = yapf_file
707 elif fpath in (top_dir, parent_dir):
708 # If we're at the top level directory, or if we're at root
709 # there is no provided style.
710 ret = None
711 else:
712 # Otherwise recurse on the current directory.
713 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
714 yapf_config_cache[fpath] = ret
715 return ret
Aiden Bennerc08566e2018-10-03 17:52:42 +0000716
717
Brian Sheedyb4307d52019-12-02 19:18:17 +0000718def _GetYapfIgnorePatterns(top_dir):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000719 """Returns all patterns in the .yapfignore file.
Brian Sheedy59b06a82019-10-14 17:03:29 +0000720
721 yapf is supposed to handle the ignoring of files listed in .yapfignore itself,
722 but this functionality appears to break when explicitly passing files to
723 yapf for formatting. According to
Josip Sokcevicc39ab992020-09-24 20:09:15 +0000724 https://github.com/google/yapf/blob/HEAD/README.rst#excluding-files-from-formatting-yapfignore,
Brian Sheedy59b06a82019-10-14 17:03:29 +0000725 the .yapfignore file should be in the directory that yapf is invoked from,
726 which we assume to be the top level directory in this case.
727
728 Args:
729 top_dir: The top level directory for the repository being formatted.
730
731 Returns:
Brian Sheedyb4307d52019-12-02 19:18:17 +0000732 A set of all fnmatch patterns to be ignored.
Brian Sheedy59b06a82019-10-14 17:03:29 +0000733 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000734 yapfignore_file = os.path.join(top_dir, '.yapfignore')
735 ignore_patterns = set()
736 if not os.path.exists(yapfignore_file):
737 return ignore_patterns
Brian Sheedy59b06a82019-10-14 17:03:29 +0000738
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000739 for line in gclient_utils.FileRead(yapfignore_file).split('\n'):
740 stripped_line = line.strip()
741 # Comments and blank lines should be ignored.
742 if stripped_line.startswith('#') or stripped_line == '':
743 continue
744 ignore_patterns.add(stripped_line)
745 return ignore_patterns
Brian Sheedyb4307d52019-12-02 19:18:17 +0000746
747
748def _FilterYapfIgnoredFiles(filepaths, patterns):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000749 """Filters out any filepaths that match any of the given patterns.
Brian Sheedyb4307d52019-12-02 19:18:17 +0000750
751 Args:
752 filepaths: An iterable of strings containing filepaths to filter.
753 patterns: An iterable of strings containing fnmatch patterns to filter on.
754
755 Returns:
756 A list of strings containing all the elements of |filepaths| that did not
757 match any of the patterns in |patterns|.
758 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000759 # Not inlined so that tests can use the same implementation.
760 return [
761 f for f in filepaths
762 if not any(fnmatch.fnmatch(f, p) for p in patterns)
763 ]
Brian Sheedy59b06a82019-10-14 17:03:29 +0000764
765
Daniel Cheng66d0f152023-08-29 23:21:58 +0000766def _GetCommitCountSummary(begin_commit: str,
767 end_commit: str = "HEAD") -> Optional[str]:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000768 """Generate a summary of the number of commits in (begin_commit, end_commit).
Daniel Cheng66d0f152023-08-29 23:21:58 +0000769
770 Returns a string containing the summary, or None if the range is empty.
771 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000772 count = int(
773 RunGitSilent(['rev-list', '--count', f'{begin_commit}..{end_commit}']))
Daniel Cheng66d0f152023-08-29 23:21:58 +0000774
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000775 if not count:
776 return None
Daniel Cheng66d0f152023-08-29 23:21:58 +0000777
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000778 return f'{count} commit{"s"[:count!=1]}'
Daniel Cheng66d0f152023-08-29 23:21:58 +0000779
780
Aaron Gable13101a62018-02-09 13:20:41 -0800781def print_stats(args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000782 """Prints statistics about the change to the user."""
783 # --no-ext-diff is broken in some versions of Git, so try to work around
784 # this by overriding the environment (but there is still a problem if the
785 # git config key "diff.external" is used).
786 env = GetNoGitPagerEnv()
787 if 'GIT_EXTERNAL_DIFF' in env:
788 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000789
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000790 return subprocess2.call(
791 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
792 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000793
794
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000795class BuildbucketResponseException(Exception):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000796 pass
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000797
798
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000799class Settings(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000800 def __init__(self):
801 self.cc = None
802 self.root = None
803 self.tree_status_url = None
804 self.viewvc_url = None
805 self.updated = False
806 self.is_gerrit = None
807 self.squash_gerrit_uploads = None
808 self.gerrit_skip_ensure_authenticated = None
809 self.git_editor = None
810 self.format_full_by_default = None
811 self.is_status_commit_order_by_date = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000812
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000813 def _LazyUpdateIfNeeded(self):
814 """Updates the settings from a codereview.settings file, if available."""
815 if self.updated:
816 return
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000817
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000818 # The only value that actually changes the behavior is
819 # autoupdate = "false". Everything else means "true".
820 autoupdate = (scm.GIT.GetConfig(self.GetRoot(), 'rietveld.autoupdate',
821 '').lower())
Edward Lemur26964072020-02-19 19:18:51 +0000822
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000823 cr_settings_file = FindCodereviewSettingsFile()
824 if autoupdate != 'false' and cr_settings_file:
825 LoadCodereviewSettingsFromFile(cr_settings_file)
826 cr_settings_file.close()
Edward Lemur26964072020-02-19 19:18:51 +0000827
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000828 self.updated = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000829
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000830 @staticmethod
831 def GetRelativeRoot():
832 return scm.GIT.GetCheckoutRoot('.')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000833
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000834 def GetRoot(self):
835 if self.root is None:
836 self.root = os.path.abspath(self.GetRelativeRoot())
837 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000838
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000839 def GetTreeStatusUrl(self, error_ok=False):
840 if not self.tree_status_url:
841 self.tree_status_url = self._GetConfig('rietveld.tree-status-url')
842 if self.tree_status_url is None and not error_ok:
843 DieWithError(
844 'You must configure your tree status URL by running '
845 '"git cl config".')
846 return self.tree_status_url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000847
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000848 def GetViewVCUrl(self):
849 if not self.viewvc_url:
850 self.viewvc_url = self._GetConfig('rietveld.viewvc-url')
851 return self.viewvc_url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000852
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000853 def GetBugPrefix(self):
854 return self._GetConfig('rietveld.bug-prefix')
rmistry@google.com78948ed2015-07-08 23:09:57 +0000855
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000856 def GetRunPostUploadHook(self):
857 run_post_upload_hook = self._GetConfig('rietveld.run-post-upload-hook')
858 return run_post_upload_hook == "True"
rmistry@google.com5626a922015-02-26 14:03:30 +0000859
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000860 def GetDefaultCCList(self):
861 return self._GetConfig('rietveld.cc')
Joanna Wangc8f23e22023-01-19 21:18:10 +0000862
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000863 def GetSquashGerritUploads(self):
864 """Returns True if uploads to Gerrit should be squashed by default."""
865 if self.squash_gerrit_uploads is None:
866 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
867 if self.squash_gerrit_uploads is None:
868 # Default is squash now (http://crbug.com/611892#c23).
869 self.squash_gerrit_uploads = self._GetConfig(
870 'gerrit.squash-uploads').lower() != 'false'
871 return self.squash_gerrit_uploads
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000872
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000873 def GetSquashGerritUploadsOverride(self):
874 """Return True or False if codereview.settings should be overridden.
Edward Lesmes4de54132020-05-05 19:41:33 +0000875
876 Returns None if no override has been defined.
877 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000878 # See also http://crbug.com/611892#c23
879 result = self._GetConfig('gerrit.override-squash-uploads').lower()
880 if result == 'true':
881 return True
882 if result == 'false':
883 return False
884 return None
Edward Lesmes4de54132020-05-05 19:41:33 +0000885
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000886 def GetIsGerrit(self):
887 """Return True if gerrit.host is set."""
888 if self.is_gerrit is None:
889 self.is_gerrit = bool(self._GetConfig('gerrit.host', False))
890 return self.is_gerrit
Aleksey Khoroshilov35ef5ad2022-06-03 18:29:25 +0000891
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000892 def GetGerritSkipEnsureAuthenticated(self):
893 """Return True if EnsureAuthenticated should not be done for Gerrit
tandrii@chromium.org28253532016-04-14 13:46:56 +0000894 uploads."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000895 if self.gerrit_skip_ensure_authenticated is None:
896 self.gerrit_skip_ensure_authenticated = self._GetConfig(
897 'gerrit.skip-ensure-authenticated').lower() == 'true'
898 return self.gerrit_skip_ensure_authenticated
tandrii@chromium.org28253532016-04-14 13:46:56 +0000899
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000900 def GetGitEditor(self):
901 """Returns the editor specified in the git config, or None if none is."""
902 if self.git_editor is None:
903 # Git requires single quotes for paths with spaces. We need to
904 # replace them with double quotes for Windows to treat such paths as
905 # a single path.
906 self.git_editor = self._GetConfig('core.editor').replace('\'', '"')
907 return self.git_editor or None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000908
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000909 def GetLintRegex(self):
910 return self._GetConfig('rietveld.cpplint-regex', DEFAULT_LINT_REGEX)
thestig@chromium.org44202a22014-03-11 19:22:18 +0000911
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000912 def GetLintIgnoreRegex(self):
913 return self._GetConfig('rietveld.cpplint-ignore-regex',
914 DEFAULT_LINT_IGNORE_REGEX)
thestig@chromium.org44202a22014-03-11 19:22:18 +0000915
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000916 def GetFormatFullByDefault(self):
917 if self.format_full_by_default is None:
918 self._LazyUpdateIfNeeded()
919 result = (RunGit(
920 ['config', '--bool', 'rietveld.format-full-by-default'],
921 error_ok=True).strip())
922 self.format_full_by_default = (result == 'true')
923 return self.format_full_by_default
Jamie Madilldc4d19e2019-10-24 21:50:02 +0000924
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000925 def IsStatusCommitOrderByDate(self):
926 if self.is_status_commit_order_by_date is None:
927 result = (RunGit(['config', '--bool', 'cl.date-order'],
928 error_ok=True).strip())
929 self.is_status_commit_order_by_date = (result == 'true')
930 return self.is_status_commit_order_by_date
Sigurd Schneider1bfda8e2021-06-30 14:46:25 +0000931
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000932 def _GetConfig(self, key, default=''):
933 self._LazyUpdateIfNeeded()
934 return scm.GIT.GetConfig(self.GetRoot(), key, default)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000935
936
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000937class _CQState(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000938 """Enum for states of CL with respect to CQ."""
939 NONE = 'none'
940 DRY_RUN = 'dry_run'
941 COMMIT = 'commit'
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000942
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000943 ALL_STATES = [NONE, DRY_RUN, COMMIT]
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000944
945
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000946class _ParsedIssueNumberArgument(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000947 def __init__(self, issue=None, patchset=None, hostname=None):
948 self.issue = issue
949 self.patchset = patchset
950 self.hostname = hostname
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000951
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000952 @property
953 def valid(self):
954 return self.issue is not None
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000955
956
Edward Lemurf38bc172019-09-03 21:02:13 +0000957def ParseIssueNumberArgument(arg):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000958 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
959 fail_result = _ParsedIssueNumberArgument()
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000960
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000961 if isinstance(arg, int):
962 return _ParsedIssueNumberArgument(issue=arg)
963 if not isinstance(arg, str):
964 return fail_result
Edward Lemur678a6842019-10-03 22:25:05 +0000965
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000966 if arg.isdigit():
967 return _ParsedIssueNumberArgument(issue=int(arg))
Aaron Gableaee6c852017-06-26 12:49:01 -0700968
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000969 url = gclient_utils.UpgradeToHttps(arg)
970 if not url.startswith('http'):
971 return fail_result
972 for gerrit_url, short_url in _KNOWN_GERRIT_TO_SHORT_URLS.items():
973 if url.startswith(short_url):
974 url = gerrit_url + url[len(short_url):]
975 break
Andrii Shyshkalov8aebb602020-04-16 22:10:27 +0000976
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000977 try:
978 parsed_url = urllib.parse.urlparse(url)
979 except ValueError:
980 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +0200981
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000982 # If "https://" was automatically added, fail if `arg` looks unlikely to be
983 # a URL.
984 if not arg.startswith('http') and '.' not in parsed_url.netloc:
985 return fail_result
Alex Turner30ae6372022-01-04 02:32:52 +0000986
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000987 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
988 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
989 # Short urls like https://domain/<issue_number> can be used, but don't allow
990 # specifying the patchset (you'd 404), but we allow that here.
991 if parsed_url.path == '/':
992 part = parsed_url.fragment
993 else:
994 part = parsed_url.path
Edward Lemur678a6842019-10-03 22:25:05 +0000995
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000996 match = re.match(r'(/c(/.*/\+)?)?/(?P<issue>\d+)(/(?P<patchset>\d+)?/?)?$',
997 part)
998 if not match:
999 return fail_result
Edward Lemur678a6842019-10-03 22:25:05 +00001000
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001001 issue = int(match.group('issue'))
1002 patchset = match.group('patchset')
1003 return _ParsedIssueNumberArgument(
1004 issue=issue,
1005 patchset=int(patchset) if patchset else None,
1006 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001007
1008
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00001009def _create_description_from_log(args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001010 """Pulls out the commit log to use as a base for the CL description."""
1011 log_args = []
1012 if len(args) == 1 and args[0] == None:
1013 # Handle the case where None is passed as the branch.
1014 return ''
1015 if len(args) == 1 and not args[0].endswith('.'):
1016 log_args = [args[0] + '..']
1017 elif len(args) == 1 and args[0].endswith('...'):
1018 log_args = [args[0][:-1]]
1019 elif len(args) == 2:
1020 log_args = [args[0] + '..' + args[1]]
1021 else:
1022 log_args = args[:] # Hope for the best!
1023 return RunGit(['log', '--pretty=format:%B%n'] + log_args)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00001024
1025
Aaron Gablea45ee112016-11-22 15:14:38 -08001026class GerritChangeNotExists(Exception):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001027 def __init__(self, issue, url):
1028 self.issue = issue
1029 self.url = url
1030 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001031
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001032 def __str__(self):
1033 return 'change %s at %s does not exist or you have no access to it' % (
1034 self.issue, self.url)
tandriic2405f52016-10-10 08:13:15 -07001035
1036
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001037_CommentSummary = collections.namedtuple(
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001038 '_CommentSummary',
1039 [
1040 'date',
1041 'message',
1042 'sender',
1043 'autogenerated',
1044 # TODO(tandrii): these two aren't known in Gerrit.
1045 'approval',
1046 'disapproval'
1047 ])
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001048
Joanna Wang6215dd02023-02-07 15:58:03 +00001049# TODO(b/265929888): Change `parent` to `pushed_commit_base`.
Joanna Wange8523912023-01-21 02:05:40 +00001050_NewUpload = collections.namedtuple('NewUpload', [
Joanna Wang40497912023-01-24 21:18:16 +00001051 'reviewers', 'ccs', 'commit_to_push', 'new_last_uploaded_commit', 'parent',
Joanna Wang7603f042023-03-01 22:17:36 +00001052 'change_desc', 'prev_patchset'
Joanna Wange8523912023-01-21 02:05:40 +00001053])
1054
1055
Daniel Chengabf48472023-08-30 15:45:13 +00001056class ChangeDescription(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001057 """Contains a parsed form of the change description."""
1058 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
1059 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
1060 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
1061 FIXED_LINE = r'^[ \t]*Fixed[ \t]*:[ \t]*(.*?)[ \t]*$'
1062 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
1063 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
1064 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
1065 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):($|[^:])'
1066 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
Daniel Chengabf48472023-08-30 15:45:13 +00001067
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001068 def __init__(self, description, bug=None, fixed=None):
1069 self._description_lines = (description or '').strip().splitlines()
1070 if bug:
1071 regexp = re.compile(self.BUG_LINE)
1072 prefix = settings.GetBugPrefix()
1073 if not any(
1074 (regexp.match(line) for line in self._description_lines)):
1075 values = list(_get_bug_line_values(prefix, bug))
1076 self.append_footer('Bug: %s' % ', '.join(values))
1077 if fixed:
1078 regexp = re.compile(self.FIXED_LINE)
1079 prefix = settings.GetBugPrefix()
1080 if not any(
1081 (regexp.match(line) for line in self._description_lines)):
1082 values = list(_get_bug_line_values(prefix, fixed))
1083 self.append_footer('Fixed: %s' % ', '.join(values))
Daniel Chengabf48472023-08-30 15:45:13 +00001084
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001085 @property # www.logilab.org/ticket/89786
1086 def description(self): # pylint: disable=method-hidden
1087 return '\n'.join(self._description_lines)
Daniel Chengabf48472023-08-30 15:45:13 +00001088
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001089 def set_description(self, desc):
1090 if isinstance(desc, str):
1091 lines = desc.splitlines()
1092 else:
1093 lines = [line.rstrip() for line in desc]
1094 while lines and not lines[0]:
1095 lines.pop(0)
1096 while lines and not lines[-1]:
1097 lines.pop(-1)
1098 self._description_lines = lines
Daniel Chengabf48472023-08-30 15:45:13 +00001099
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001100 def ensure_change_id(self, change_id):
1101 description = self.description
1102 footer_change_ids = git_footers.get_footer_change_id(description)
1103 # Make sure that the Change-Id in the description matches the given one.
1104 if footer_change_ids != [change_id]:
1105 if footer_change_ids:
1106 # Remove any existing Change-Id footers since they don't match
1107 # the expected change_id footer.
1108 description = git_footers.remove_footer(description,
1109 'Change-Id')
1110 print(
1111 'WARNING: Change-Id has been set to %s. Use `git cl issue 0` '
1112 'if you want to set a new one.')
1113 # Add the expected Change-Id footer.
1114 description = git_footers.add_footer_change_id(
1115 description, change_id)
1116 self.set_description(description)
Daniel Chengabf48472023-08-30 15:45:13 +00001117
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001118 def update_reviewers(self, reviewers):
1119 """Rewrites the R= line(s) as a single line each.
Daniel Chengabf48472023-08-30 15:45:13 +00001120
1121 Args:
1122 reviewers (list(str)) - list of additional emails to use for reviewers.
1123 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001124 if not reviewers:
1125 return
Daniel Chengabf48472023-08-30 15:45:13 +00001126
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001127 reviewers = set(reviewers)
Daniel Chengabf48472023-08-30 15:45:13 +00001128
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001129 # Get the set of R= lines and remove them from the description.
1130 regexp = re.compile(self.R_LINE)
1131 matches = [regexp.match(line) for line in self._description_lines]
1132 new_desc = [
1133 l for i, l in enumerate(self._description_lines) if not matches[i]
1134 ]
1135 self.set_description(new_desc)
Daniel Chengabf48472023-08-30 15:45:13 +00001136
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001137 # Construct new unified R= lines.
Daniel Chengabf48472023-08-30 15:45:13 +00001138
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001139 # First, update reviewers with names from the R= lines (if any).
1140 for match in matches:
1141 if not match:
1142 continue
1143 reviewers.update(cleanup_list([match.group(2).strip()]))
Daniel Chengabf48472023-08-30 15:45:13 +00001144
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001145 new_r_line = 'R=' + ', '.join(sorted(reviewers))
Daniel Chengabf48472023-08-30 15:45:13 +00001146
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001147 # Put the new lines in the description where the old first R= line was.
1148 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1149 if 0 <= line_loc < len(self._description_lines):
1150 self._description_lines.insert(line_loc, new_r_line)
1151 else:
1152 self.append_footer(new_r_line)
Daniel Chengabf48472023-08-30 15:45:13 +00001153
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001154 def set_preserve_tryjobs(self):
1155 """Ensures description footer contains 'Cq-Do-Not-Cancel-Tryjobs: true'."""
1156 footers = git_footers.parse_footers(self.description)
1157 for v in footers.get('Cq-Do-Not-Cancel-Tryjobs', []):
1158 if v.lower() == 'true':
1159 return
1160 self.append_footer('Cq-Do-Not-Cancel-Tryjobs: true')
Daniel Chengabf48472023-08-30 15:45:13 +00001161
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001162 def prompt(self):
1163 """Asks the user to update the description."""
1164 self.set_description([
1165 '# Enter a description of the change.',
1166 '# This will be displayed on the codereview site.',
1167 '# The first line will also be used as the subject of the review.',
1168 '#--------------------This line is 72 characters long'
1169 '--------------------',
1170 ] + self._description_lines)
1171 bug_regexp = re.compile(self.BUG_LINE)
1172 fixed_regexp = re.compile(self.FIXED_LINE)
1173 prefix = settings.GetBugPrefix()
1174 has_issue = lambda l: bug_regexp.match(l) or fixed_regexp.match(l)
Daniel Chengabf48472023-08-30 15:45:13 +00001175
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001176 if not any((has_issue(line) for line in self._description_lines)):
1177 self.append_footer('Bug: %s' % prefix)
Daniel Chengabf48472023-08-30 15:45:13 +00001178
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001179 print('Waiting for editor...')
1180 content = gclient_utils.RunEditor(self.description,
1181 True,
1182 git_editor=settings.GetGitEditor())
1183 if not content:
1184 DieWithError('Running editor failed')
1185 lines = content.splitlines()
Daniel Chengabf48472023-08-30 15:45:13 +00001186
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001187 # Strip off comments and default inserted "Bug:" line.
1188 clean_lines = [
1189 line.rstrip() for line in lines
1190 if not (line.startswith('#') or line.rstrip() == "Bug:"
1191 or line.rstrip() == "Bug: " + prefix)
1192 ]
1193 if not clean_lines:
1194 DieWithError('No CL description, aborting')
1195 self.set_description(clean_lines)
Daniel Chengabf48472023-08-30 15:45:13 +00001196
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001197 def append_footer(self, line):
1198 """Adds a footer line to the description.
Daniel Chengabf48472023-08-30 15:45:13 +00001199
1200 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
1201 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
1202 that Gerrit footers are always at the end.
1203 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001204 parsed_footer_line = git_footers.parse_footer(line)
1205 if parsed_footer_line:
1206 # Line is a gerrit footer in the form: Footer-Key: any value.
1207 # Thus, must be appended observing Gerrit footer rules.
1208 self.set_description(
1209 git_footers.add_footer(self.description,
1210 key=parsed_footer_line[0],
1211 value=parsed_footer_line[1]))
1212 return
Daniel Chengabf48472023-08-30 15:45:13 +00001213
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001214 if not self._description_lines:
1215 self._description_lines.append(line)
1216 return
Daniel Chengabf48472023-08-30 15:45:13 +00001217
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001218 top_lines, gerrit_footers, _ = git_footers.split_footers(
1219 self.description)
1220 if gerrit_footers:
1221 # git_footers.split_footers ensures that there is an empty line
1222 # before actual (gerrit) footers, if any. We have to keep it that
1223 # way.
1224 assert top_lines and top_lines[-1] == ''
1225 top_lines, separator = top_lines[:-1], top_lines[-1:]
1226 else:
1227 separator = [
1228 ] # No need for separator if there are no gerrit_footers.
Daniel Chengabf48472023-08-30 15:45:13 +00001229
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001230 prev_line = top_lines[-1] if top_lines else ''
1231 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line)
1232 or not presubmit_support.Change.TAG_LINE_RE.match(line)):
1233 top_lines.append('')
1234 top_lines.append(line)
1235 self._description_lines = top_lines + separator + gerrit_footers
Daniel Chengabf48472023-08-30 15:45:13 +00001236
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001237 def get_reviewers(self, tbr_only=False):
1238 """Retrieves the list of reviewers."""
1239 matches = [
1240 re.match(self.R_LINE, line) for line in self._description_lines
1241 ]
1242 reviewers = [
1243 match.group(2).strip() for match in matches
1244 if match and (not tbr_only or match.group(1).upper() == 'TBR')
1245 ]
1246 return cleanup_list(reviewers)
Daniel Chengabf48472023-08-30 15:45:13 +00001247
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001248 def get_cced(self):
1249 """Retrieves the list of reviewers."""
1250 matches = [
1251 re.match(self.CC_LINE, line) for line in self._description_lines
1252 ]
1253 cced = [match.group(2).strip() for match in matches if match]
1254 return cleanup_list(cced)
Daniel Chengabf48472023-08-30 15:45:13 +00001255
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001256 def get_hash_tags(self):
1257 """Extracts and sanitizes a list of Gerrit hashtags."""
1258 subject = (self._description_lines or ('', ))[0]
1259 subject = re.sub(self.STRIP_HASH_TAG_PREFIX,
1260 '',
1261 subject,
1262 flags=re.IGNORECASE)
Daniel Chengabf48472023-08-30 15:45:13 +00001263
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001264 tags = []
1265 start = 0
1266 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
1267 while True:
1268 m = bracket_exp.match(subject, start)
1269 if not m:
1270 break
1271 tags.append(self.sanitize_hash_tag(m.group(1)))
1272 start = m.end()
Daniel Chengabf48472023-08-30 15:45:13 +00001273
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001274 if not tags:
1275 # Try "Tag: " prefix.
1276 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
1277 if m:
1278 tags.append(self.sanitize_hash_tag(m.group(1)))
1279 return tags
Daniel Chengabf48472023-08-30 15:45:13 +00001280
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001281 @classmethod
1282 def sanitize_hash_tag(cls, tag):
1283 """Returns a sanitized Gerrit hash tag.
Daniel Chengabf48472023-08-30 15:45:13 +00001284
1285 A sanitized hashtag can be used as a git push refspec parameter value.
1286 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001287 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
Daniel Chengabf48472023-08-30 15:45:13 +00001288
1289
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001290class Changelist(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001291 """Changelist works with one changelist in local branch.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001292
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001293 Notes:
1294 * Not safe for concurrent multi-{thread,process} use.
1295 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001296 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001297 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001298 def __init__(self,
1299 branchref=None,
1300 issue=None,
1301 codereview_host=None,
1302 commit_date=None):
1303 """Create a new ChangeList instance.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001304
Edward Lemurf38bc172019-09-03 21:02:13 +00001305 **kwargs will be passed directly to Gerrit implementation.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001306 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001307 # Poke settings so we get the "configure your server" message if
1308 # necessary.
1309 global settings
1310 if not settings:
1311 # Happens when git_cl.py is used as a utility library.
1312 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001313
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001314 self.branchref = branchref
1315 if self.branchref:
1316 assert branchref.startswith('refs/heads/')
1317 self.branch = scm.GIT.ShortBranchName(self.branchref)
1318 else:
1319 self.branch = None
1320 self.commit_date = commit_date
1321 self.upstream_branch = None
1322 self.lookedup_issue = False
1323 self.issue = issue or None
1324 self.description = None
1325 self.lookedup_patchset = False
1326 self.patchset = None
1327 self.cc = None
1328 self.more_cc = []
1329 self._remote = None
1330 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001331
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001332 # Lazily cached values.
1333 self._gerrit_host = None # e.g. chromium-review.googlesource.com
1334 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
1335 self._owners_client = None
1336 # Map from change number (issue) to its detail cache.
1337 self._detail_cache = {}
Edward Lemur125d60a2019-09-13 18:25:41 +00001338
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001339 if codereview_host is not None:
1340 assert not codereview_host.startswith('https://'), codereview_host
1341 self._gerrit_host = codereview_host
1342 self._gerrit_server = 'https://%s' % codereview_host
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001343
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001344 @property
1345 def owners_client(self):
1346 if self._owners_client is None:
1347 remote, remote_branch = self.GetRemoteBranch()
1348 branch = GetTargetRef(remote, remote_branch, None)
1349 self._owners_client = owners_client.GetCodeOwnersClient(
1350 host=self.GetGerritHost(),
1351 project=self.GetGerritProject(),
1352 branch=branch)
1353 return self._owners_client
Edward Lesmese1576912021-02-16 21:53:34 +00001354
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001355 def GetCCList(self):
1356 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001357
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001358 The return value is a string suitable for passing to git cl with the --cc
1359 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001360 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001361 if self.cc is None:
1362 base_cc = settings.GetDefaultCCList()
1363 more_cc = ','.join(self.more_cc)
1364 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1365 return self.cc
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001366
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001367 def ExtendCC(self, more_cc):
1368 """Extends the list of users to cc on this CL based on the changed files."""
1369 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001370
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001371 def GetCommitDate(self):
1372 """Returns the commit date as provided in the constructor"""
1373 return self.commit_date
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00001374
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001375 def GetBranch(self):
1376 """Returns the short branch name, e.g. 'main'."""
1377 if not self.branch:
1378 branchref = scm.GIT.GetBranchRef(settings.GetRoot())
1379 if not branchref:
1380 return None
1381 self.branchref = branchref
1382 self.branch = scm.GIT.ShortBranchName(self.branchref)
1383 return self.branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001384
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001385 def GetBranchRef(self):
1386 """Returns the full branch name, e.g. 'refs/heads/main'."""
1387 self.GetBranch() # Poke the lazy loader.
1388 return self.branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001389
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001390 def _GitGetBranchConfigValue(self, key, default=None):
1391 return scm.GIT.GetBranchConfig(settings.GetRoot(), self.GetBranch(),
1392 key, default)
tandrii5d48c322016-08-18 16:19:37 -07001393
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001394 def _GitSetBranchConfigValue(self, key, value):
1395 action = 'set %s to %r' % (key, value)
1396 if not value:
1397 action = 'unset %s' % key
1398 assert self.GetBranch(), 'a branch is needed to ' + action
1399 return scm.GIT.SetBranchConfig(settings.GetRoot(), self.GetBranch(),
1400 key, value)
tandrii5d48c322016-08-18 16:19:37 -07001401
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001402 @staticmethod
1403 def FetchUpstreamTuple(branch):
1404 """Returns a tuple containing remote and remote ref,
Josip Sokcevicc39ab992020-09-24 20:09:15 +00001405 e.g. 'origin', 'refs/heads/main'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001406 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001407 remote, upstream_branch = scm.GIT.FetchUpstreamTuple(
1408 settings.GetRoot(), branch)
1409 if not remote or not upstream_branch:
1410 DieWithError(
1411 'Unable to determine default branch to diff against.\n'
1412 'Verify this branch is set up to track another \n'
1413 '(via the --track argument to "git checkout -b ..."). \n'
1414 'or pass complete "git diff"-style arguments if supported, like\n'
1415 ' git cl upload origin/main\n')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001416
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001417 return remote, upstream_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001418
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001419 def GetCommonAncestorWithUpstream(self):
1420 upstream_branch = self.GetUpstreamBranch()
1421 if not scm.GIT.IsValidRevision(settings.GetRoot(), upstream_branch):
1422 DieWithError(
Joanna Wangd4dfff02023-09-13 17:44:31 +00001423 'The current branch (%s) has an upstream (%s) that does not exist '
1424 'anymore.\nPlease fix it and try again.' %
1425 (self.GetBranch(), upstream_branch))
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001426 return git_common.get_or_create_merge_base(self.GetBranch(),
1427 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001428
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001429 def GetUpstreamBranch(self):
1430 if self.upstream_branch is None:
1431 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
1432 if remote != '.':
1433 upstream_branch = upstream_branch.replace(
1434 'refs/heads/', 'refs/remotes/%s/' % remote)
1435 upstream_branch = upstream_branch.replace(
1436 'refs/branch-heads/', 'refs/remotes/branch-heads/')
1437 self.upstream_branch = upstream_branch
1438 return self.upstream_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001439
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001440 def GetRemoteBranch(self):
1441 if not self._remote:
1442 remote, branch = None, self.GetBranch()
1443 seen_branches = set()
1444 while branch not in seen_branches:
1445 seen_branches.add(branch)
1446 remote, branch = self.FetchUpstreamTuple(branch)
1447 branch = scm.GIT.ShortBranchName(branch)
1448 if remote != '.' or branch.startswith('refs/remotes'):
1449 break
1450 else:
1451 remotes = RunGit(['remote'], error_ok=True).split()
1452 if len(remotes) == 1:
1453 remote, = remotes
1454 elif 'origin' in remotes:
1455 remote = 'origin'
1456 logging.warning(
1457 'Could not determine which remote this change is '
1458 'associated with, so defaulting to "%s".' %
1459 self._remote)
1460 else:
1461 logging.warning(
1462 'Could not determine which remote this change is '
1463 'associated with.')
1464 branch = 'HEAD'
1465 if branch.startswith('refs/remotes'):
1466 self._remote = (remote, branch)
1467 elif branch.startswith('refs/branch-heads/'):
1468 self._remote = (remote, branch.replace('refs/',
1469 'refs/remotes/'))
1470 else:
1471 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
1472 return self._remote
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001473
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001474 def GetRemoteUrl(self) -> Optional[str]:
1475 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001476
1477 Returns None if there is no remote.
1478 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001479 is_cached, value = self._cached_remote_url
1480 if is_cached:
1481 return value
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001482
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001483 remote, _ = self.GetRemoteBranch()
1484 url = scm.GIT.GetConfig(settings.GetRoot(), 'remote.%s.url' % remote,
1485 '')
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001486
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001487 # Check if the remote url can be parsed as an URL.
1488 host = urllib.parse.urlparse(url).netloc
1489 if host:
1490 self._cached_remote_url = (True, url)
1491 return url
Edward Lemur298f2cf2019-02-22 21:40:39 +00001492
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001493 # If it cannot be parsed as an url, assume it is a local directory,
1494 # probably a git cache.
1495 logging.warning(
1496 '"%s" doesn\'t appear to point to a git host. '
1497 'Interpreting it as a local directory.', url)
1498 if not os.path.isdir(url):
1499 logging.error(
1500 'Remote "%(remote)s" for branch "%(branch)s" points to "%(url)s", '
1501 'but it doesn\'t exist.', {
1502 'remote': remote,
1503 'branch': self.GetBranch(),
1504 'url': url
1505 })
1506 return None
Edward Lemur298f2cf2019-02-22 21:40:39 +00001507
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001508 cache_path = url
1509 url = scm.GIT.GetConfig(url, 'remote.%s.url' % remote, '')
Edward Lemur298f2cf2019-02-22 21:40:39 +00001510
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001511 host = urllib.parse.urlparse(url).netloc
1512 if not host:
1513 logging.error(
1514 'Remote "%(remote)s" for branch "%(branch)s" points to '
1515 '"%(cache_path)s", but it is misconfigured.\n'
1516 '"%(cache_path)s" must be a git repo and must have a remote named '
1517 '"%(remote)s" pointing to the git host.', {
1518 'remote': remote,
1519 'cache_path': cache_path,
1520 'branch': self.GetBranch()
1521 })
1522 return None
Edward Lemur298f2cf2019-02-22 21:40:39 +00001523
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001524 self._cached_remote_url = (True, url)
1525 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001526
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001527 def GetIssue(self):
1528 """Returns the issue number as a int or None if not set."""
1529 if self.issue is None and not self.lookedup_issue:
1530 if self.GetBranch():
1531 self.issue = self._GitGetBranchConfigValue(ISSUE_CONFIG_KEY)
1532 if self.issue is not None:
1533 self.issue = int(self.issue)
1534 self.lookedup_issue = True
1535 return self.issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001536
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001537 def GetIssueURL(self, short=False):
1538 """Get the URL for a particular issue."""
1539 issue = self.GetIssue()
1540 if not issue:
1541 return None
1542 server = self.GetCodereviewServer()
1543 if short:
1544 server = _KNOWN_GERRIT_TO_SHORT_URLS.get(server, server)
1545 return '%s/%s' % (server, issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001546
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001547 def FetchDescription(self, pretty=False):
1548 assert self.GetIssue(), 'issue is required to query Gerrit'
Edward Lemur6c6827c2020-02-06 21:15:18 +00001549
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001550 if self.description is None:
1551 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
1552 current_rev = data['current_revision']
1553 self.description = data['revisions'][current_rev]['commit'][
1554 'message']
Edward Lemur6c6827c2020-02-06 21:15:18 +00001555
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001556 if not pretty:
1557 return self.description
Edward Lemur6c6827c2020-02-06 21:15:18 +00001558
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001559 # Set width to 72 columns + 2 space indent.
1560 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
1561 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1562 lines = self.description.splitlines()
1563 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001564
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001565 def GetPatchset(self):
1566 """Returns the patchset number as a int or None if not set."""
1567 if self.patchset is None and not self.lookedup_patchset:
1568 if self.GetBranch():
1569 self.patchset = self._GitGetBranchConfigValue(
1570 PATCHSET_CONFIG_KEY)
1571 if self.patchset is not None:
1572 self.patchset = int(self.patchset)
1573 self.lookedup_patchset = True
1574 return self.patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001575
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001576 def GetAuthor(self):
1577 return scm.GIT.GetConfig(settings.GetRoot(), 'user.email')
Edward Lemur9aa1a962020-02-25 00:58:38 +00001578
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001579 def SetPatchset(self, patchset):
1580 """Set this branch's patchset. If patchset=0, clears the patchset."""
1581 assert self.GetBranch()
1582 if not patchset:
1583 self.patchset = None
1584 else:
1585 self.patchset = int(patchset)
1586 self._GitSetBranchConfigValue(PATCHSET_CONFIG_KEY, str(self.patchset))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001587
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001588 def SetIssue(self, issue=None):
1589 """Set this branch's issue. If issue isn't given, clears the issue."""
1590 assert self.GetBranch()
1591 if issue:
1592 issue = int(issue)
1593 self._GitSetBranchConfigValue(ISSUE_CONFIG_KEY, str(issue))
1594 self.issue = issue
1595 codereview_server = self.GetCodereviewServer()
1596 if codereview_server:
1597 self._GitSetBranchConfigValue(CODEREVIEW_SERVER_CONFIG_KEY,
1598 codereview_server)
1599 else:
1600 # Reset all of these just to be clean.
1601 reset_suffixes = [
1602 LAST_UPLOAD_HASH_CONFIG_KEY,
1603 ISSUE_CONFIG_KEY,
1604 PATCHSET_CONFIG_KEY,
1605 CODEREVIEW_SERVER_CONFIG_KEY,
1606 GERRIT_SQUASH_HASH_CONFIG_KEY,
1607 ]
1608 for prop in reset_suffixes:
1609 try:
1610 self._GitSetBranchConfigValue(prop, None)
1611 except subprocess2.CalledProcessError:
1612 pass
1613 msg = RunGit(['log', '-1', '--format=%B']).strip()
1614 if msg and git_footers.get_footer_change_id(msg):
1615 print(
1616 'WARNING: The change patched into this branch has a Change-Id. '
1617 'Removing it.')
1618 RunGit([
1619 'commit', '--amend', '-m',
1620 git_footers.remove_footer(msg, 'Change-Id')
1621 ])
1622 self.lookedup_issue = True
1623 self.issue = None
1624 self.patchset = None
1625
1626 def GetAffectedFiles(self,
1627 upstream: str,
1628 end_commit: Optional[str] = None) -> Sequence[str]:
1629 """Returns the list of affected files for the given commit range."""
Edward Lemur85153282020-02-14 22:06:29 +00001630 try:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001631 return [
1632 f for _, f in scm.GIT.CaptureStatus(
1633 settings.GetRoot(), upstream, end_commit=end_commit)
1634 ]
Edward Lemur85153282020-02-14 22:06:29 +00001635 except subprocess2.CalledProcessError:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001636 DieWithError(
1637 ('\nFailed to diff against upstream branch %s\n\n'
1638 'This branch probably doesn\'t exist anymore. To reset the\n'
1639 'tracking branch, please run\n'
1640 ' git branch --set-upstream-to origin/main %s\n'
1641 'or replace origin/main with the relevant branch') %
1642 (upstream, self.GetBranch()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001643
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001644 def UpdateDescription(self, description, force=False):
1645 assert self.GetIssue(), 'issue is required to update description'
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001646
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001647 if gerrit_util.HasPendingChangeEdit(self.GetGerritHost(),
1648 self._GerritChangeIdentifier()):
1649 if not force:
1650 confirm_or_exit(
1651 'The description cannot be modified while the issue has a pending '
1652 'unpublished edit. Either publish the edit in the Gerrit web UI '
1653 'or delete it.\n\n',
1654 action='delete the unpublished edit')
Edward Lemur6c6827c2020-02-06 21:15:18 +00001655
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001656 gerrit_util.DeletePendingChangeEdit(self.GetGerritHost(),
1657 self._GerritChangeIdentifier())
1658 gerrit_util.SetCommitMessage(self.GetGerritHost(),
1659 self._GerritChangeIdentifier(),
1660 description,
1661 notify='NONE')
Edward Lemur6c6827c2020-02-06 21:15:18 +00001662
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001663 self.description = description
Edward Lemur6c6827c2020-02-06 21:15:18 +00001664
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001665 def _GetCommonPresubmitArgs(self, verbose, upstream):
1666 args = [
1667 '--root',
1668 settings.GetRoot(),
1669 '--upstream',
1670 upstream,
1671 ]
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001672
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001673 args.extend(['--verbose'] * verbose)
Edward Lemur227d5102020-02-25 23:45:35 +00001674
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001675 remote, remote_branch = self.GetRemoteBranch()
1676 target_ref = GetTargetRef(remote, remote_branch, None)
1677 if settings.GetIsGerrit():
1678 args.extend(['--gerrit_url', self.GetCodereviewServer()])
1679 args.extend(['--gerrit_project', self.GetGerritProject()])
1680 args.extend(['--gerrit_branch', target_ref])
Edward Lemur227d5102020-02-25 23:45:35 +00001681
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001682 author = self.GetAuthor()
1683 issue = self.GetIssue()
1684 patchset = self.GetPatchset()
1685 if author:
1686 args.extend(['--author', author])
1687 if issue:
1688 args.extend(['--issue', str(issue)])
1689 if patchset:
1690 args.extend(['--patchset', str(patchset)])
Edward Lesmeseb1bd622021-03-01 19:54:07 +00001691
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001692 return args
Edward Lemur227d5102020-02-25 23:45:35 +00001693
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001694 def RunHook(self,
1695 committing,
1696 may_prompt,
1697 verbose,
1698 parallel,
1699 upstream,
1700 description,
1701 all_files,
1702 files=None,
1703 resultdb=False,
1704 realm=None):
1705 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1706 args = self._GetCommonPresubmitArgs(verbose, upstream)
1707 args.append('--commit' if committing else '--upload')
1708 if may_prompt:
1709 args.append('--may_prompt')
1710 if parallel:
1711 args.append('--parallel')
1712 if all_files:
1713 args.append('--all_files')
1714 if files:
1715 args.extend(files.split(';'))
1716 args.append('--source_controlled_only')
1717 if files or all_files:
1718 args.append('--no_diffs')
Edward Lemur75526302020-02-27 22:31:05 +00001719
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001720 if resultdb and not realm:
1721 # TODO (crbug.com/1113463): store realm somewhere and look it up so
1722 # it is not required to pass the realm flag
1723 print(
1724 'Note: ResultDB reporting will NOT be performed because --realm'
1725 ' was not specified. To enable ResultDB, please run the command'
1726 ' again with the --realm argument to specify the LUCI realm.')
Edward Lemur227d5102020-02-25 23:45:35 +00001727
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001728 return self._RunPresubmit(args,
1729 description,
1730 resultdb=resultdb,
1731 realm=realm)
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001732
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001733 def _RunPresubmit(self,
1734 args: Sequence[str],
1735 description: str,
1736 resultdb: bool = False,
1737 realm: Optional[str] = None) -> Mapping[str, Any]:
1738 args = list(args)
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001739
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001740 with gclient_utils.temporary_file() as description_file:
1741 with gclient_utils.temporary_file() as json_output:
1742 gclient_utils.FileWrite(description_file, description)
1743 args.extend(['--json_output', json_output])
1744 args.extend(['--description_file', description_file])
1745 start = time_time()
1746 cmd = ['vpython3', PRESUBMIT_SUPPORT] + args
1747 if resultdb and realm:
1748 cmd = ['rdb', 'stream', '-new', '-realm', realm, '--'] + cmd
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001749
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001750 p = subprocess2.Popen(cmd)
1751 exit_code = p.wait()
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001752
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001753 metrics.collector.add_repeated(
1754 'sub_commands', {
1755 'command': 'presubmit',
1756 'execution_time': time_time() - start,
1757 'exit_code': exit_code,
1758 })
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001759
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001760 if exit_code:
1761 sys.exit(exit_code)
Edward Lemur227d5102020-02-25 23:45:35 +00001762
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001763 json_results = gclient_utils.FileRead(json_output)
1764 return json.loads(json_results)
Edward Lemur227d5102020-02-25 23:45:35 +00001765
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001766 def RunPostUploadHook(self, verbose, upstream, description):
1767 args = self._GetCommonPresubmitArgs(verbose, upstream)
1768 args.append('--post_upload')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001769
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001770 with gclient_utils.temporary_file() as description_file:
1771 gclient_utils.FileWrite(description_file, description)
1772 args.extend(['--description_file', description_file])
1773 subprocess2.Popen(['vpython3', PRESUBMIT_SUPPORT] + args).wait()
Edward Lemur75526302020-02-27 22:31:05 +00001774
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001775 def _GetDescriptionForUpload(self, options: optparse.Values,
1776 git_diff_args: Sequence[str],
1777 files: Sequence[str]) -> ChangeDescription:
1778 """Get description message for upload."""
1779 if self.GetIssue():
1780 description = self.FetchDescription()
1781 elif options.message:
1782 description = options.message
1783 else:
1784 description = _create_description_from_log(git_diff_args)
1785 if options.title and options.squash:
1786 description = options.title + '\n\n' + description
Edward Lemur75526302020-02-27 22:31:05 +00001787
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001788 bug = options.bug
1789 fixed = options.fixed
1790 if not self.GetIssue():
Gavin Mak36d937d2023-09-26 19:52:40 +00001791 # There isn't any issue attached, so we shouldn't keep existing
1792 # Change-Ids in the description.
1793 if git_footers.get_footer_change_id(description):
1794 description = git_footers.remove_footer(description,
1795 'Change-Id')
1796
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001797 # Extract bug number from branch name, but only if issue is being
1798 # created. It must start with bug or fix, followed by _ or - and
1799 # number. Optionally, it may contain _ or - after number with
1800 # arbitrary text. Examples: bug-123 bug_123 fix-123
1801 # fix-123-some-description
1802 branch = self.GetBranch()
1803 if branch is not None:
1804 match = re.match(
1805 r'^(?P<type>bug|fix(?:e[sd])?)[_-]?(?P<bugnum>\d+)([-_]|$)',
1806 branch)
1807 if not bug and not fixed and match:
1808 if match.group('type') == 'bug':
1809 bug = match.group('bugnum')
1810 else:
1811 fixed = match.group('bugnum')
Edward Lemur5a644f82020-03-18 16:44:57 +00001812
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001813 change_description = ChangeDescription(description, bug, fixed)
Edward Lemur5a644f82020-03-18 16:44:57 +00001814
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001815 # Fill gaps in OWNERS coverage to reviewers if requested.
1816 if options.add_owners_to:
1817 assert options.add_owners_to in ('R'), options.add_owners_to
1818 status = self.owners_client.GetFilesApprovalStatus(
1819 files, [], options.reviewers)
1820 missing_files = [
1821 f for f in files
1822 if status[f] == self._owners_client.INSUFFICIENT_REVIEWERS
1823 ]
1824 owners = self.owners_client.SuggestOwners(
1825 missing_files, exclude=[self.GetAuthor()])
1826 assert isinstance(options.reviewers, list), options.reviewers
1827 options.reviewers.extend(owners)
Edward Lemur5a644f82020-03-18 16:44:57 +00001828
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001829 # Set the reviewer list now so that presubmit checks can access it.
1830 if options.reviewers:
1831 change_description.update_reviewers(options.reviewers)
Edward Lesmes8c43c3f2021-01-20 00:20:26 +00001832
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001833 return change_description
Edward Lemur5a644f82020-03-18 16:44:57 +00001834
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001835 def _GetTitleForUpload(self, options, multi_change_upload=False):
1836 # type: (optparse.Values, Optional[bool]) -> str
Edward Lemur5a644f82020-03-18 16:44:57 +00001837
Gavin Mak36d937d2023-09-26 19:52:40 +00001838 # Getting titles for multiple commits is not supported so we return the
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001839 # default.
1840 if not options.squash or multi_change_upload or options.title:
1841 return options.title
Joanna Wanga1abbed2023-01-24 01:41:05 +00001842
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001843 # On first upload, patchset title is always this string, while
1844 # options.title gets converted to first line of message.
1845 if not self.GetIssue():
1846 return 'Initial upload'
Edward Lemur5a644f82020-03-18 16:44:57 +00001847
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001848 # When uploading subsequent patchsets, options.message is taken as the
1849 # title if options.title is not provided.
1850 if options.message:
1851 return options.message.strip()
Edward Lemur5a644f82020-03-18 16:44:57 +00001852
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001853 # Use the subject of the last commit as title by default.
1854 title = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
1855 if options.force or options.skip_title:
1856 return title
1857 user_title = gclient_utils.AskForData('Title for patchset [%s]: ' %
1858 title)
Edward Lemur5a644f82020-03-18 16:44:57 +00001859
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001860 # Use the default title if the user confirms the default with a 'y'.
1861 if user_title.lower() == 'y':
1862 return title
1863 return user_title or title
mlcui3da91712021-05-05 10:00:30 +00001864
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001865 def _GetRefSpecOptions(self,
1866 options: optparse.Values,
1867 change_desc: ChangeDescription,
1868 multi_change_upload: bool = False,
1869 dogfood_path: bool = False) -> List[str]:
1870 # Extra options that can be specified at push time. Doc:
1871 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
1872 refspec_opts = []
Edward Lemur5a644f82020-03-18 16:44:57 +00001873
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001874 # By default, new changes are started in WIP mode, and subsequent
1875 # patchsets don't send email. At any time, passing --send-mail or
1876 # --send-email will mark the change ready and send email for that
1877 # particular patch.
1878 if options.send_mail:
1879 refspec_opts.append('ready')
1880 refspec_opts.append('notify=ALL')
1881 elif (not self.GetIssue() and options.squash and not dogfood_path):
1882 refspec_opts.append('wip')
Joanna Wanga1abbed2023-01-24 01:41:05 +00001883
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001884 # TODO(tandrii): options.message should be posted as a comment if
1885 # --send-mail or --send-email is set on non-initial upload as Rietveld
1886 # used to do it.
Joanna Wanga1abbed2023-01-24 01:41:05 +00001887
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001888 # Set options.title in case user was prompted in _GetTitleForUpload and
1889 # _CMDUploadChange needs to be called again.
1890 options.title = self._GetTitleForUpload(
1891 options, multi_change_upload=multi_change_upload)
Joanna Wanga1abbed2023-01-24 01:41:05 +00001892
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001893 if options.title:
1894 # Punctuation and whitespace in |title| must be percent-encoded.
1895 refspec_opts.append(
1896 'm=' + gerrit_util.PercentEncodeForGitRef(options.title))
Joanna Wanga1abbed2023-01-24 01:41:05 +00001897
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001898 if options.private:
1899 refspec_opts.append('private')
Joanna Wanga1abbed2023-01-24 01:41:05 +00001900
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001901 if options.topic:
1902 # Documentation on Gerrit topics is here:
1903 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
1904 refspec_opts.append('topic=%s' % options.topic)
Joanna Wanga1abbed2023-01-24 01:41:05 +00001905
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001906 if options.enable_auto_submit:
1907 refspec_opts.append('l=Auto-Submit+1')
1908 if options.set_bot_commit:
1909 refspec_opts.append('l=Bot-Commit+1')
1910 if options.use_commit_queue:
1911 refspec_opts.append('l=Commit-Queue+2')
1912 elif options.cq_dry_run:
1913 refspec_opts.append('l=Commit-Queue+1')
Joanna Wanga1abbed2023-01-24 01:41:05 +00001914
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001915 if change_desc.get_reviewers(tbr_only=True):
1916 score = gerrit_util.GetCodeReviewTbrScore(self.GetGerritHost(),
1917 self.GetGerritProject())
1918 refspec_opts.append('l=Code-Review+%s' % score)
Joanna Wanga1abbed2023-01-24 01:41:05 +00001919
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001920 # Gerrit sorts hashtags, so order is not important.
1921 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
1922 # We check GetIssue because we only add hashtags from the
1923 # description on the first upload.
1924 # TODO(b/265929888): When we fully launch the new path:
1925 # 1) remove fetching hashtags from description alltogether
1926 # 2) Or use descrtiption hashtags for:
1927 # `not (self.GetIssue() and multi_change_upload)`
1928 # 3) Or enabled change description tags for multi and single changes
1929 # by adding them post `git push`.
1930 if not (self.GetIssue() and dogfood_path):
1931 hashtags.update(change_desc.get_hash_tags())
1932 refspec_opts.extend(['hashtag=%s' % t for t in hashtags])
Joanna Wanga1abbed2023-01-24 01:41:05 +00001933
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001934 # Note: Reviewers, and ccs are handled individually for each
1935 # branch/change.
1936 return refspec_opts
Joanna Wang40497912023-01-24 21:18:16 +00001937
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001938 def PrepareSquashedCommit(self,
1939 options: optparse.Values,
1940 parent: str,
1941 orig_parent: str,
1942 end_commit: Optional[str] = None) -> _NewUpload:
1943 """Create a squashed commit to upload.
Joanna Wang05b60342023-03-29 20:25:57 +00001944
1945
1946 Args:
1947 parent: The commit to use as the parent for the new squashed.
1948 orig_parent: The commit that is an actual ancestor of `end_commit`. It
1949 is part of the same original tree as end_commit, which does not
1950 contain squashed commits. This is used to create the change
1951 description for the new squashed commit with:
1952 `git log orig_parent..end_commit`.
1953 end_commit: The commit to use as the end of the new squashed commit.
1954 """
Joanna Wangb88a4342023-01-24 01:28:22 +00001955
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001956 if end_commit is None:
1957 end_commit = RunGit(['rev-parse', self.branchref]).strip()
Joanna Wangb88a4342023-01-24 01:28:22 +00001958
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001959 reviewers, ccs, change_desc = self._PrepareChange(
1960 options, orig_parent, end_commit)
1961 latest_tree = RunGit(['rev-parse', end_commit + ':']).strip()
1962 with gclient_utils.temporary_file() as desc_tempfile:
1963 gclient_utils.FileWrite(desc_tempfile, change_desc.description)
1964 commit_to_push = RunGit(
1965 ['commit-tree', latest_tree, '-p', parent, '-F',
1966 desc_tempfile]).strip()
Joanna Wangb88a4342023-01-24 01:28:22 +00001967
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001968 # Gerrit may or may not update fast enough to return the correct
1969 # patchset number after we push. Get the pre-upload patchset and
1970 # increment later.
1971 prev_patchset = self.GetMostRecentPatchset(update=False) or 0
1972 return _NewUpload(reviewers, ccs, commit_to_push, end_commit, parent,
1973 change_desc, prev_patchset)
Joanna Wangb88a4342023-01-24 01:28:22 +00001974
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001975 def PrepareCherryPickSquashedCommit(self, options: optparse.Values,
1976 parent: str) -> _NewUpload:
1977 """Create a commit cherry-picked on parent to push."""
Joanna Wange8523912023-01-21 02:05:40 +00001978
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001979 # The `parent` is what we will cherry-pick on top of.
1980 # The `cherry_pick_base` is the beginning range of what
1981 # we are cherry-picking.
1982 cherry_pick_base = self.GetCommonAncestorWithUpstream()
1983 reviewers, ccs, change_desc = self._PrepareChange(
1984 options, cherry_pick_base, self.branchref)
Joanna Wange8523912023-01-21 02:05:40 +00001985
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001986 new_upload_hash = RunGit(['rev-parse', self.branchref]).strip()
1987 latest_tree = RunGit(['rev-parse', self.branchref + ':']).strip()
1988 with gclient_utils.temporary_file() as desc_tempfile:
1989 gclient_utils.FileWrite(desc_tempfile, change_desc.description)
1990 commit_to_cp = RunGit([
1991 'commit-tree', latest_tree, '-p', cherry_pick_base, '-F',
1992 desc_tempfile
1993 ]).strip()
Joanna Wange8523912023-01-21 02:05:40 +00001994
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001995 RunGit(['checkout', '-q', parent])
1996 ret, _out = RunGitWithCode(['cherry-pick', commit_to_cp])
1997 if ret:
1998 RunGit(['cherry-pick', '--abort'])
1999 RunGit(['checkout', '-q', self.branch])
2000 DieWithError('Could not cleanly cherry-pick')
Joanna Wange8523912023-01-21 02:05:40 +00002001
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002002 commit_to_push = RunGit(['rev-parse', 'HEAD']).strip()
2003 RunGit(['checkout', '-q', self.branch])
Joanna Wange8523912023-01-21 02:05:40 +00002004
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002005 # Gerrit may or may not update fast enough to return the correct
2006 # patchset number after we push. Get the pre-upload patchset and
2007 # increment later.
2008 prev_patchset = self.GetMostRecentPatchset(update=False) or 0
2009 return _NewUpload(reviewers, ccs, commit_to_push, new_upload_hash,
2010 cherry_pick_base, change_desc, prev_patchset)
Joanna Wange8523912023-01-21 02:05:40 +00002011
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002012 def _PrepareChange(
2013 self, options: optparse.Values, parent: str, end_commit: str
2014 ) -> Tuple[Sequence[str], Sequence[str], ChangeDescription]:
2015 """Prepares the change to be uploaded."""
2016 self.EnsureCanUploadPatchset(options.force)
Joanna Wangb46232e2023-01-21 01:58:46 +00002017
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002018 files = self.GetAffectedFiles(parent, end_commit=end_commit)
2019 change_desc = self._GetDescriptionForUpload(options,
2020 [parent, end_commit], files)
Joanna Wangb46232e2023-01-21 01:58:46 +00002021
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002022 watchlist = watchlists.Watchlists(settings.GetRoot())
2023 self.ExtendCC(watchlist.GetWatchersForPaths(files))
2024 if not options.bypass_hooks:
2025 hook_results = self.RunHook(committing=False,
2026 may_prompt=not options.force,
2027 verbose=options.verbose,
2028 parallel=options.parallel,
2029 upstream=parent,
2030 description=change_desc.description,
2031 all_files=False)
2032 self.ExtendCC(hook_results['more_cc'])
Joanna Wangb46232e2023-01-21 01:58:46 +00002033
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002034 # Update the change description and ensure we have a Change Id.
2035 if self.GetIssue():
2036 if options.edit_description:
2037 change_desc.prompt()
2038 change_detail = self._GetChangeDetail(['CURRENT_REVISION'])
2039 change_id = change_detail['change_id']
2040 change_desc.ensure_change_id(change_id)
Joanna Wangb46232e2023-01-21 01:58:46 +00002041
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002042 else: # No change issue. First time uploading
2043 if not options.force and not options.message_file:
2044 change_desc.prompt()
Joanna Wangb46232e2023-01-21 01:58:46 +00002045
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002046 # Check if user added a change_id in the descripiton.
2047 change_ids = git_footers.get_footer_change_id(
2048 change_desc.description)
2049 if len(change_ids) == 1:
2050 change_id = change_ids[0]
2051 else:
2052 change_id = GenerateGerritChangeId(change_desc.description)
2053 change_desc.ensure_change_id(change_id)
Joanna Wangb46232e2023-01-21 01:58:46 +00002054
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002055 if options.preserve_tryjobs:
2056 change_desc.set_preserve_tryjobs()
Joanna Wangb46232e2023-01-21 01:58:46 +00002057
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002058 SaveDescriptionBackup(change_desc)
Joanna Wangb46232e2023-01-21 01:58:46 +00002059
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002060 # Add ccs
2061 ccs = []
2062 # Add default, watchlist, presubmit ccs if this is the initial upload
2063 # and CL is not private and auto-ccing has not been disabled.
2064 if not options.private and not options.no_autocc and not self.GetIssue(
2065 ):
2066 ccs = self.GetCCList().split(',')
2067 if len(ccs) > 100:
2068 lsc = (
2069 'https://chromium.googlesource.com/chromium/src/+/HEAD/docs/'
2070 'process/lsc/lsc_workflow.md')
2071 print('WARNING: This will auto-CC %s users.' % len(ccs))
2072 print('LSC may be more appropriate: %s' % lsc)
2073 print(
2074 'You can also use the --no-autocc flag to disable auto-CC.')
2075 confirm_or_exit(action='continue')
Joanna Wangb46232e2023-01-21 01:58:46 +00002076
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002077 # Add ccs from the --cc flag.
2078 if options.cc:
2079 ccs.extend(options.cc)
Joanna Wangb46232e2023-01-21 01:58:46 +00002080
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002081 ccs = [email.strip() for email in ccs if email.strip()]
2082 if change_desc.get_cced():
2083 ccs.extend(change_desc.get_cced())
Joanna Wangb46232e2023-01-21 01:58:46 +00002084
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002085 return change_desc.get_reviewers(), ccs, change_desc
Joanna Wangb46232e2023-01-21 01:58:46 +00002086
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002087 def PostUploadUpdates(self, options: optparse.Values,
2088 new_upload: _NewUpload, change_number: str) -> None:
2089 """Makes necessary post upload changes to the local and remote cl."""
2090 if not self.GetIssue():
2091 self.SetIssue(change_number)
Joanna Wang40497912023-01-24 21:18:16 +00002092
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002093 self.SetPatchset(new_upload.prev_patchset + 1)
Joanna Wang7603f042023-03-01 22:17:36 +00002094
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002095 self._GitSetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY,
2096 new_upload.commit_to_push)
2097 self._GitSetBranchConfigValue(LAST_UPLOAD_HASH_CONFIG_KEY,
2098 new_upload.new_last_uploaded_commit)
Joanna Wang40497912023-01-24 21:18:16 +00002099
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002100 if settings.GetRunPostUploadHook():
2101 self.RunPostUploadHook(options.verbose, new_upload.parent,
2102 new_upload.change_desc.description)
Joanna Wang40497912023-01-24 21:18:16 +00002103
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002104 if new_upload.reviewers or new_upload.ccs:
2105 gerrit_util.AddReviewers(self.GetGerritHost(),
2106 self._GerritChangeIdentifier(),
2107 reviewers=new_upload.reviewers,
2108 ccs=new_upload.ccs,
2109 notify=bool(options.send_mail))
Joanna Wang40497912023-01-24 21:18:16 +00002110
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002111 def CMDUpload(self, options, git_diff_args, orig_args):
2112 """Uploads a change to codereview."""
2113 custom_cl_base = None
2114 if git_diff_args:
2115 custom_cl_base = base_branch = git_diff_args[0]
2116 else:
2117 if self.GetBranch() is None:
2118 DieWithError(
2119 'Can\'t upload from detached HEAD state. Get on a branch!')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002120
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002121 # Default to diffing against common ancestor of upstream branch
2122 base_branch = self.GetCommonAncestorWithUpstream()
2123 git_diff_args = [base_branch, 'HEAD']
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002124
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002125 # Fast best-effort checks to abort before running potentially expensive
2126 # hooks if uploading is likely to fail anyway. Passing these checks does
2127 # not guarantee that uploading will not fail.
2128 self.EnsureAuthenticated(force=options.force)
2129 self.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002130
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002131 print(f'Processing {_GetCommitCountSummary(*git_diff_args)}...')
Daniel Cheng66d0f152023-08-29 23:21:58 +00002132
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002133 # Apply watchlists on upload.
2134 watchlist = watchlists.Watchlists(settings.GetRoot())
2135 files = self.GetAffectedFiles(base_branch)
2136 if not options.bypass_watchlists:
2137 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002138
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002139 change_desc = self._GetDescriptionForUpload(options, git_diff_args,
2140 files)
2141 if not options.bypass_hooks:
2142 hook_results = self.RunHook(committing=False,
2143 may_prompt=not options.force,
2144 verbose=options.verbose,
2145 parallel=options.parallel,
2146 upstream=base_branch,
2147 description=change_desc.description,
2148 all_files=False)
2149 self.ExtendCC(hook_results['more_cc'])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002150
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002151 print_stats(git_diff_args)
2152 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base,
2153 change_desc)
2154 if not ret:
2155 if self.GetBranch() is not None:
2156 self._GitSetBranchConfigValue(
2157 LAST_UPLOAD_HASH_CONFIG_KEY,
2158 scm.GIT.ResolveCommit(settings.GetRoot(), 'HEAD'))
2159 # Run post upload hooks, if specified.
2160 if settings.GetRunPostUploadHook():
2161 self.RunPostUploadHook(options.verbose, base_branch,
2162 change_desc.description)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002163
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002164 # Upload all dependencies if specified.
2165 if options.dependencies:
2166 print()
2167 print('--dependencies has been specified.')
2168 print('All dependent local branches will be re-uploaded.')
2169 print()
2170 # Remove the dependencies flag from args so that we do not end
2171 # up in a loop.
2172 orig_args.remove('--dependencies')
2173 ret = upload_branch_deps(self, orig_args, options.force)
2174 return ret
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002175
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002176 def SetCQState(self, new_state):
2177 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002178
Struan Shrimpton8b2072b2023-07-31 21:01:26 +00002179 Issue must have been already uploaded and known.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002180 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002181 assert new_state in _CQState.ALL_STATES
2182 assert self.GetIssue()
2183 try:
2184 vote_map = {
2185 _CQState.NONE: 0,
2186 _CQState.DRY_RUN: 1,
2187 _CQState.COMMIT: 2,
2188 }
2189 labels = {'Commit-Queue': vote_map[new_state]}
2190 notify = False if new_state == _CQState.DRY_RUN else None
2191 gerrit_util.SetReview(self.GetGerritHost(),
2192 self._GerritChangeIdentifier(),
2193 labels=labels,
2194 notify=notify)
2195 return 0
2196 except KeyboardInterrupt:
2197 raise
2198 except:
2199 print(
2200 'WARNING: Failed to %s.\n'
2201 'Either:\n'
2202 ' * Your project has no CQ,\n'
2203 ' * You don\'t have permission to change the CQ state,\n'
2204 ' * There\'s a bug in this code (see stack trace below).\n'
2205 'Consider specifying which bots to trigger manually or asking your '
2206 'project owners for permissions or contacting Chrome Infra at:\n'
2207 'https://www.chromium.org/infra\n\n' %
2208 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
2209 # Still raise exception so that stack trace is printed.
2210 raise
qyearsley1fdfcb62016-10-24 13:22:03 -07002211
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002212 def GetGerritHost(self):
2213 # Lazy load of configs.
2214 self.GetCodereviewServer()
Aravind Vasudevan0e3589e2023-05-25 23:00:30 +00002215
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002216 if self._gerrit_host and '.' not in self._gerrit_host:
2217 # Abbreviated domain like "chromium" instead of
2218 # chromium.googlesource.com.
2219 parsed = urllib.parse.urlparse(self.GetRemoteUrl())
2220 if parsed.scheme == 'sso':
2221 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2222 self._gerrit_server = 'https://%s' % self._gerrit_host
Aravind Vasudevan0e3589e2023-05-25 23:00:30 +00002223
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002224 return self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002225
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002226 def _GetGitHost(self):
2227 """Returns git host to be used when uploading change to Gerrit."""
2228 remote_url = self.GetRemoteUrl()
2229 if not remote_url:
2230 return None
2231 return urllib.parse.urlparse(remote_url).netloc
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002232
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002233 def GetCodereviewServer(self):
2234 if not self._gerrit_server:
2235 # If we're on a branch then get the server potentially associated
2236 # with that branch.
2237 if self.GetIssue() and self.GetBranch():
2238 self._gerrit_server = self._GitGetBranchConfigValue(
2239 CODEREVIEW_SERVER_CONFIG_KEY)
2240 if self._gerrit_server:
2241 self._gerrit_host = urllib.parse.urlparse(
2242 self._gerrit_server).netloc
2243 if not self._gerrit_server:
2244 url = urllib.parse.urlparse(self.GetRemoteUrl())
2245 parts = url.netloc.split('.')
Aravind Vasudevan0e3589e2023-05-25 23:00:30 +00002246
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002247 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2248 # has "-review" suffix for lowest level subdomain.
2249 parts[0] = parts[0] + '-review'
Aravind Vasudevan0e3589e2023-05-25 23:00:30 +00002250
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002251 if url.scheme == 'sso' and len(parts) == 1:
2252 # sso:// uses abbreivated hosts, eg. sso://chromium instead
2253 # of chromium.googlesource.com. Hence, for code review
2254 # server, they need to be expanded.
2255 parts[0] += '.googlesource.com'
Aravind Vasudevan0e3589e2023-05-25 23:00:30 +00002256
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002257 self._gerrit_host = '.'.join(parts)
2258 self._gerrit_server = 'https://%s' % self._gerrit_host
2259 return self._gerrit_server
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002260
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002261 def GetGerritProject(self):
2262 """Returns Gerrit project name based on remote git URL."""
2263 remote_url = self.GetRemoteUrl()
2264 if remote_url is None:
2265 logging.warning('can\'t detect Gerrit project.')
2266 return None
2267 project = urllib.parse.urlparse(remote_url).path.strip('/')
2268 if project.endswith('.git'):
2269 project = project[:-len('.git')]
2270 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start
2271 # with 'a/' prefix, because 'a/' prefix is used to force authentication
2272 # in gitiles/git-over-https protocol. E.g.,
2273 # https://chromium.googlesource.com/a/v8/v8 refers to the same
2274 # repo/project as https://chromium.googlesource.com/v8/v8
2275 if project.startswith('a/'):
2276 project = project[len('a/'):]
2277 return project
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002278
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002279 def _GerritChangeIdentifier(self):
2280 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002281
2282 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002283 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002284 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002285 project = self.GetGerritProject()
2286 if project:
2287 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
2288 # Fall back on still unique, but less efficient change number.
2289 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002290
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002291 def EnsureAuthenticated(self, force, refresh=None):
2292 """Best effort check that user is authenticated with Gerrit server."""
2293 if settings.GetGerritSkipEnsureAuthenticated():
2294 # For projects with unusual authentication schemes.
2295 # See http://crbug.com/603378.
2296 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002297
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002298 # Check presence of cookies only if using cookies-based auth method.
2299 cookie_auth = gerrit_util.Authenticator.get()
2300 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
2301 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002302
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002303 remote_url = self.GetRemoteUrl()
2304 if remote_url is None:
2305 logging.warning('invalid remote')
2306 return
2307 if urllib.parse.urlparse(remote_url).scheme not in ['https', 'sso']:
2308 logging.warning(
2309 'Ignoring branch %(branch)s with non-https/sso remote '
2310 '%(remote)s', {
2311 'branch': self.branch,
2312 'remote': self.GetRemoteUrl()
2313 })
2314 return
Daniel Chengcf6269b2019-05-18 01:02:12 +00002315
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002316 # Lazy-loader to identify Gerrit and Git hosts.
2317 self.GetCodereviewServer()
2318 git_host = self._GetGitHost()
2319 assert self._gerrit_server and self._gerrit_host and git_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002320
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002321 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2322 git_auth = cookie_auth.get_auth_header(git_host)
2323 if gerrit_auth and git_auth:
2324 if gerrit_auth == git_auth:
2325 return
2326 all_gsrc = cookie_auth.get_auth_header(
2327 'd0esN0tEx1st.googlesource.com')
2328 print(
2329 'WARNING: You have different credentials for Gerrit and git hosts:\n'
2330 ' %s\n'
2331 ' %s\n'
2332 ' Consider running the following command:\n'
2333 ' git cl creds-check\n'
2334 ' %s\n'
2335 ' %s' %
2336 (git_host, self._gerrit_host,
2337 ('Hint: delete creds for .googlesource.com' if all_gsrc else
2338 ''), cookie_auth.get_new_password_message(git_host)))
2339 if not force:
2340 confirm_or_exit('If you know what you are doing',
2341 action='continue')
2342 return
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00002343
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002344 missing = (([] if gerrit_auth else [self._gerrit_host]) +
2345 ([] if git_auth else [git_host]))
2346 DieWithError('Credentials for the following hosts are required:\n'
2347 ' %s\n'
2348 'These are read from %s (or legacy %s)\n'
2349 '%s' %
2350 ('\n '.join(missing), cookie_auth.get_gitcookies_path(),
2351 cookie_auth.get_netrc_path(),
2352 cookie_auth.get_new_password_message(git_host)))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002353
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002354 def EnsureCanUploadPatchset(self, force):
2355 if not self.GetIssue():
2356 return
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002357
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002358 status = self._GetChangeDetail()['status']
2359 if status == 'ABANDONED':
2360 DieWithError(
2361 'Change %s has been abandoned, new uploads are not allowed' %
2362 (self.GetIssueURL()))
2363 if status == 'MERGED':
2364 answer = gclient_utils.AskForData(
2365 'Change %s has been submitted, new uploads are not allowed. '
2366 'Would you like to start a new change (Y/n)?' %
2367 self.GetIssueURL()).lower()
2368 if answer not in ('y', ''):
2369 DieWithError('New uploads are not allowed.')
2370 self.SetIssue()
2371 return
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002372
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002373 # TODO(vadimsh): For some reason the chunk of code below was skipped if
2374 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
2375 # Apparently this check is not very important? Otherwise get_auth_email
2376 # could have been added to other implementations of Authenticator.
2377 cookies_auth = gerrit_util.Authenticator.get()
2378 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
2379 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002380
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002381 cookies_user = cookies_auth.get_auth_email(self.GetGerritHost())
2382 if self.GetIssueOwner() == cookies_user:
2383 return
2384 logging.debug('change %s owner is %s, cookies user is %s',
2385 self.GetIssue(), self.GetIssueOwner(), cookies_user)
2386 # Maybe user has linked accounts or something like that,
2387 # so ask what Gerrit thinks of this user.
2388 details = gerrit_util.GetAccountDetails(self.GetGerritHost(), 'self')
2389 if details['email'] == self.GetIssueOwner():
2390 return
2391 if not force:
2392 print(
2393 'WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
2394 'as %s.\n'
2395 'Uploading may fail due to lack of permissions.' %
2396 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2397 confirm_or_exit(action='upload')
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002398
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002399 def GetStatus(self):
2400 """Applies a rough heuristic to give a simple summary of an issue's review
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002401 or CQ status, assuming adherence to a common workflow.
2402
2403 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002404 * 'error' - error from review tool (including deleted issues)
2405 * 'unsent' - no reviewers added
2406 * 'waiting' - waiting for review
2407 * 'reply' - waiting for uploader to reply to review
2408 * 'lgtm' - Code-Review label has been set
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00002409 * 'dry-run' - dry-running in the CQ
2410 * 'commit' - in the CQ
Aaron Gable9ab38c62017-04-06 14:36:33 -07002411 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002412 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002413 if not self.GetIssue():
2414 return None
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002415
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002416 try:
2417 data = self._GetChangeDetail(
2418 ['DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
2419 except GerritChangeNotExists:
2420 return 'error'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002421
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002422 if data['status'] in ('ABANDONED', 'MERGED'):
2423 return 'closed'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002424
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002425 cq_label = data['labels'].get('Commit-Queue', {})
2426 max_cq_vote = 0
2427 for vote in cq_label.get('all', []):
2428 max_cq_vote = max(max_cq_vote, vote.get('value', 0))
2429 if max_cq_vote == 2:
2430 return 'commit'
2431 if max_cq_vote == 1:
2432 return 'dry-run'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002433
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002434 if data['labels'].get('Code-Review', {}).get('approved'):
2435 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002436
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002437 if not data.get('reviewers', {}).get('REVIEWER', []):
2438 return 'unsent'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002439
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002440 owner = data['owner'].get('_account_id')
2441 messages = sorted(data.get('messages', []), key=lambda m: m.get('date'))
2442 while messages:
2443 m = messages.pop()
2444 if (m.get('tag', '').startswith('autogenerated:cq')
2445 or m.get('tag', '').startswith('autogenerated:cv')):
2446 # Ignore replies from LUCI CV/CQ.
2447 continue
2448 if m.get('author', {}).get('_account_id') == owner:
2449 # Most recent message was by owner.
2450 return 'waiting'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00002451
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002452 # Some reply from non-owner.
2453 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002454
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002455 # Somehow there are no messages even though there are reviewers.
2456 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002457
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002458 def GetMostRecentPatchset(self, update=True):
2459 if not self.GetIssue():
2460 return None
Edward Lemur6c6827c2020-02-06 21:15:18 +00002461
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002462 data = self._GetChangeDetail(['CURRENT_REVISION'])
2463 patchset = data['revisions'][data['current_revision']]['_number']
2464 if update:
2465 self.SetPatchset(patchset)
2466 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002467
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002468 def _IsPatchsetRangeSignificant(self, lower, upper):
2469 """Returns True if the inclusive range of patchsets contains any reworks or
Gavin Makf35a9eb2022-11-17 18:34:36 +00002470 rebases."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002471 if not self.GetIssue():
2472 return False
Gavin Makf35a9eb2022-11-17 18:34:36 +00002473
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002474 data = self._GetChangeDetail(['ALL_REVISIONS'])
2475 ps_kind = {}
2476 for rev_info in data.get('revisions', {}).values():
2477 ps_kind[rev_info['_number']] = rev_info.get('kind', '')
Gavin Makf35a9eb2022-11-17 18:34:36 +00002478
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002479 for ps in range(lower, upper + 1):
2480 assert ps in ps_kind, 'expected patchset %d in change detail' % ps
2481 if ps_kind[ps] not in ('NO_CHANGE', 'NO_CODE_CHANGE'):
2482 return True
2483 return False
Gavin Makf35a9eb2022-11-17 18:34:36 +00002484
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002485 def GetMostRecentDryRunPatchset(self):
2486 """Get patchsets equivalent to the most recent patchset and return
Gavin Make61ccc52020-11-13 00:12:57 +00002487 the patchset with the latest dry run. If none have been dry run, return
2488 the latest patchset."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002489 if not self.GetIssue():
2490 return None
Gavin Make61ccc52020-11-13 00:12:57 +00002491
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002492 data = self._GetChangeDetail(['ALL_REVISIONS'])
2493 patchset = data['revisions'][data['current_revision']]['_number']
2494 dry_run = {
2495 int(m['_revision_number'])
2496 for m in data.get('messages', [])
2497 if m.get('tag', '').endswith('dry-run')
2498 }
Gavin Make61ccc52020-11-13 00:12:57 +00002499
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002500 for revision_info in sorted(data.get('revisions', {}).values(),
2501 key=lambda c: c['_number'],
2502 reverse=True):
2503 if revision_info['_number'] in dry_run:
2504 patchset = revision_info['_number']
2505 break
2506 if revision_info.get('kind', '') not in \
2507 ('NO_CHANGE', 'NO_CODE_CHANGE', 'TRIVIAL_REBASE'):
2508 break
2509 self.SetPatchset(patchset)
2510 return patchset
Gavin Make61ccc52020-11-13 00:12:57 +00002511
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002512 def AddComment(self, message, publish=None):
2513 gerrit_util.SetReview(self.GetGerritHost(),
2514 self._GerritChangeIdentifier(),
2515 msg=message,
2516 ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002517
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002518 def GetCommentsSummary(self, readable=True):
2519 # DETAILED_ACCOUNTS is to get emails in accounts.
2520 # CURRENT_REVISION is included to get the latest patchset so that
2521 # only the robot comments from the latest patchset can be shown.
2522 messages = self._GetChangeDetail(
2523 options=['MESSAGES', 'DETAILED_ACCOUNTS', 'CURRENT_REVISION']).get(
2524 'messages', [])
2525 file_comments = gerrit_util.GetChangeComments(
2526 self.GetGerritHost(), self._GerritChangeIdentifier())
2527 robot_file_comments = gerrit_util.GetChangeRobotComments(
2528 self.GetGerritHost(), self._GerritChangeIdentifier())
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002529
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002530 # Add the robot comments onto the list of comments, but only
2531 # keep those that are from the latest patchset.
2532 latest_patch_set = self.GetMostRecentPatchset()
2533 for path, robot_comments in robot_file_comments.items():
2534 line_comments = file_comments.setdefault(path, [])
2535 line_comments.extend([
2536 c for c in robot_comments if c['patch_set'] == latest_patch_set
2537 ])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002538
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002539 # Build dictionary of file comments for easy access and sorting later.
2540 # {author+date: {path: {patchset: {line: url+message}}}}
2541 comments = collections.defaultdict(lambda: collections.defaultdict(
2542 lambda: collections.defaultdict(dict)))
Andrii Shyshkalova3762a92020-11-25 10:20:42 +00002543
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002544 server = self.GetCodereviewServer()
2545 if server in _KNOWN_GERRIT_TO_SHORT_URLS:
2546 # /c/ is automatically added by short URL server.
2547 url_prefix = '%s/%s' % (_KNOWN_GERRIT_TO_SHORT_URLS[server],
2548 self.GetIssue())
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002549 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002550 url_prefix = '%s/c/%s' % (server, self.GetIssue())
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002551
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002552 for path, line_comments in file_comments.items():
2553 for comment in line_comments:
2554 tag = comment.get('tag', '')
2555 if tag.startswith(
2556 'autogenerated') and 'robot_id' not in comment:
2557 continue
2558 key = (comment['author']['email'], comment['updated'])
2559 if comment.get('side', 'REVISION') == 'PARENT':
2560 patchset = 'Base'
2561 else:
2562 patchset = 'PS%d' % comment['patch_set']
2563 line = comment.get('line', 0)
2564 url = ('%s/%s/%s#%s%s' %
2565 (url_prefix, comment['patch_set'],
2566 path, 'b' if comment.get('side') == 'PARENT' else '',
2567 str(line) if line else ''))
2568 comments[key][path][patchset][line] = (url, comment['message'])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002569
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002570 summaries = []
2571 for msg in messages:
2572 summary = self._BuildCommentSummary(msg, comments, readable)
2573 if summary:
2574 summaries.append(summary)
2575 return summaries
Josip Sokcevic266129c2021-11-09 00:22:00 +00002576
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002577 @staticmethod
2578 def _BuildCommentSummary(msg, comments, readable):
2579 if 'email' not in msg['author']:
2580 # Some bot accounts may not have an email associated.
2581 return None
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002582
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002583 key = (msg['author']['email'], msg['date'])
2584 # Don't bother showing autogenerated messages that don't have associated
2585 # file or line comments. this will filter out most autogenerated
2586 # messages, but will keep robot comments like those from Tricium.
2587 is_autogenerated = msg.get('tag', '').startswith('autogenerated')
2588 if is_autogenerated and not comments.get(key):
2589 return None
2590 message = msg['message']
2591 # Gerrit spits out nanoseconds.
2592 assert len(msg['date'].split('.')[-1]) == 9
2593 date = datetime.datetime.strptime(msg['date'][:-3],
2594 '%Y-%m-%d %H:%M:%S.%f')
2595 if key in comments:
2596 message += '\n'
2597 for path, patchsets in sorted(comments.get(key, {}).items()):
2598 if readable:
2599 message += '\n%s' % path
2600 for patchset, lines in sorted(patchsets.items()):
2601 for line, (url, content) in sorted(lines.items()):
2602 if line:
2603 line_str = 'Line %d' % line
2604 path_str = '%s:%d:' % (path, line)
2605 else:
2606 line_str = 'File comment'
2607 path_str = '%s:0:' % path
2608 if readable:
2609 message += '\n %s, %s: %s' % (patchset, line_str, url)
2610 message += '\n %s\n' % content
2611 else:
2612 message += '\n%s ' % path_str
2613 message += '\n%s\n' % content
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002614
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002615 return _CommentSummary(
2616 date=date,
2617 message=message,
2618 sender=msg['author']['email'],
2619 autogenerated=is_autogenerated,
2620 # These could be inferred from the text messages and correlated with
2621 # Code-Review label maximum, however this is not reliable.
2622 # Leaving as is until the need arises.
2623 approval=False,
2624 disapproval=False,
2625 )
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002626
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002627 def CloseIssue(self):
2628 gerrit_util.AbandonChange(self.GetGerritHost(),
2629 self._GerritChangeIdentifier(),
2630 msg='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002631
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002632 def SubmitIssue(self):
2633 gerrit_util.SubmitChange(self.GetGerritHost(),
2634 self._GerritChangeIdentifier())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002635
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002636 def _GetChangeDetail(self, options=None):
2637 """Returns details of associated Gerrit change and caching results."""
2638 options = options or []
2639 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002640
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002641 # Optimization to avoid multiple RPCs:
2642 if 'CURRENT_REVISION' in options or 'ALL_REVISIONS' in options:
2643 options.append('CURRENT_COMMIT')
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002644
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002645 # Normalize issue and options for consistent keys in cache.
2646 cache_key = str(self.GetIssue())
2647 options_set = frozenset(o.upper() for o in options)
2648
2649 for cached_options_set, data in self._detail_cache.get(cache_key, []):
2650 # Assumption: data fetched before with extra options is suitable
2651 # for return for a smaller set of options.
2652 # For example, if we cached data for
2653 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2654 # and request is for options=[CURRENT_REVISION],
2655 # THEN we can return prior cached data.
2656 if options_set.issubset(cached_options_set):
2657 return data
2658
2659 try:
2660 data = gerrit_util.GetChangeDetail(self.GetGerritHost(),
2661 self._GerritChangeIdentifier(),
2662 options_set)
2663 except gerrit_util.GerritError as e:
2664 if e.http_status == 404:
2665 raise GerritChangeNotExists(self.GetIssue(),
2666 self.GetCodereviewServer())
2667 raise
2668
2669 self._detail_cache.setdefault(cache_key, []).append((options_set, data))
Edward Lesmes7677e5c2020-02-19 20:39:03 +00002670 return data
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002671
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002672 def _GetChangeCommit(self, revision='current'):
2673 assert self.GetIssue(), 'issue must be set to query Gerrit'
2674 try:
2675 data = gerrit_util.GetChangeCommit(self.GetGerritHost(),
2676 self._GerritChangeIdentifier(),
2677 revision)
2678 except gerrit_util.GerritError as e:
2679 if e.http_status == 404:
2680 raise GerritChangeNotExists(self.GetIssue(),
2681 self.GetCodereviewServer())
2682 raise
2683 return data
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002684
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002685 def _IsCqConfigured(self):
2686 detail = self._GetChangeDetail(['LABELS'])
2687 return u'Commit-Queue' in detail.get('labels', {})
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002688
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002689 def CMDLand(self, force, bypass_hooks, verbose, parallel, resultdb, realm):
2690 if git_common.is_dirty_git_tree('land'):
2691 return 1
agable32978d92016-11-01 12:55:02 -07002692
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002693 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2694 if not force and self._IsCqConfigured():
2695 confirm_or_exit(
2696 '\nIt seems this repository has a CQ, '
2697 'which can test and land changes for you. '
2698 'Are you sure you wish to bypass it?\n',
2699 action='bypass CQ')
2700 differs = True
2701 last_upload = self._GitGetBranchConfigValue(
Gavin Mak4e5e3992022-11-14 22:40:12 +00002702 GERRIT_SQUASH_HASH_CONFIG_KEY)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002703 # Note: git diff outputs nothing if there is no diff.
2704 if not last_upload or RunGit(['diff', last_upload]).strip():
2705 print(
2706 'WARNING: Some changes from local branch haven\'t been uploaded.'
2707 )
Edward Lemur5a644f82020-03-18 16:44:57 +00002708 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002709 if detail['current_revision'] == last_upload:
2710 differs = False
2711 else:
2712 print(
2713 'WARNING: Local branch contents differ from latest uploaded '
2714 'patchset.')
2715 if differs:
2716 if not force:
2717 confirm_or_exit(
2718 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2719 action='submit')
2720 print(
2721 'WARNING: Bypassing hooks and submitting latest uploaded patchset.'
2722 )
2723 elif not bypass_hooks:
2724 upstream = self.GetCommonAncestorWithUpstream()
2725 if self.GetIssue():
2726 description = self.FetchDescription()
2727 else:
2728 description = _create_description_from_log([upstream])
2729 self.RunHook(committing=True,
2730 may_prompt=not force,
2731 verbose=verbose,
2732 parallel=parallel,
2733 upstream=upstream,
2734 description=description,
2735 all_files=False,
2736 resultdb=resultdb,
2737 realm=realm)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002738
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002739 self.SubmitIssue()
2740 print('Issue %s has been submitted.' % self.GetIssueURL())
2741 links = self._GetChangeCommit().get('web_links', [])
2742 for link in links:
2743 if link.get('name') in ['gitiles', 'browse'] and link.get('url'):
2744 print('Landed as: %s' % link.get('url'))
2745 break
2746 return 0
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002747
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002748 def CMDPatchWithParsedIssue(self, parsed_issue_arg, nocommit, force,
2749 newbranch):
2750 assert parsed_issue_arg.valid
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002751
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002752 self.issue = parsed_issue_arg.issue
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002753
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002754 if parsed_issue_arg.hostname:
2755 self._gerrit_host = parsed_issue_arg.hostname
2756 self._gerrit_server = 'https://%s' % self._gerrit_host
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002757
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002758 try:
2759 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2760 except GerritChangeNotExists as e:
2761 DieWithError(str(e))
agablec6787972016-09-09 16:13:34 -07002762
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002763 if not parsed_issue_arg.patchset:
2764 # Use current revision by default.
2765 revision_info = detail['revisions'][detail['current_revision']]
2766 patchset = int(revision_info['_number'])
2767 else:
2768 patchset = parsed_issue_arg.patchset
2769 for revision_info in detail['revisions'].values():
2770 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2771 break
2772 else:
2773 DieWithError('Couldn\'t find patchset %i in change %i' %
2774 (parsed_issue_arg.patchset, self.GetIssue()))
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002775
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002776 remote_url = self.GetRemoteUrl()
2777 if remote_url.endswith('.git'):
2778 remote_url = remote_url[:-len('.git')]
2779 remote_url = remote_url.rstrip('/')
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002780
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002781 fetch_info = revision_info['fetch']['http']
2782 fetch_info['url'] = fetch_info['url'].rstrip('/')
Josip Sokcevicf2cfd3d2021-03-30 18:39:18 +00002783
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002784 if remote_url != fetch_info['url']:
2785 DieWithError(
2786 'Trying to patch a change from %s but this repo appears '
2787 'to be %s.' % (fetch_info['url'], remote_url))
Gavin Mak4e5e3992022-11-14 22:40:12 +00002788
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002789 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002790
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002791 # Set issue immediately in case the cherry-pick fails, which happens
2792 # when resolving conflicts.
2793 if self.GetBranch():
2794 self.SetIssue(parsed_issue_arg.issue)
tandrii88189772016-09-29 04:29:57 -07002795
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002796 if force:
2797 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2798 print('Checked out commit for change %i patchset %i locally' %
2799 (parsed_issue_arg.issue, patchset))
2800 elif nocommit:
2801 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2802 print('Patch applied to index.')
2803 else:
2804 RunGit(['cherry-pick', 'FETCH_HEAD'])
2805 print('Committed patch for change %i patchset %i locally.' %
2806 (parsed_issue_arg.issue, patchset))
2807 print(
2808 'Note: this created a local commit which does not have '
2809 'the same hash as the one uploaded for review. This will make '
2810 'uploading changes based on top of this branch difficult.\n'
2811 'If you want to do that, use "git cl patch --force" instead.')
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002812
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002813 if self.GetBranch():
2814 self.SetPatchset(patchset)
2815 fetched_hash = scm.GIT.ResolveCommit(settings.GetRoot(),
2816 'FETCH_HEAD')
2817 self._GitSetBranchConfigValue(LAST_UPLOAD_HASH_CONFIG_KEY,
2818 fetched_hash)
2819 self._GitSetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY,
2820 fetched_hash)
2821 else:
2822 print(
2823 'WARNING: You are in detached HEAD state.\n'
2824 'The patch has been applied to your checkout, but you will not be '
2825 'able to upload a new patch set to the gerrit issue.\n'
2826 'Try using the \'-b\' option if you would like to work on a '
2827 'branch and/or upload a new patch set.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002828
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002829 return 0
2830
2831 @staticmethod
2832 def _GerritCommitMsgHookCheck(offer_removal):
2833 # type: (bool) -> None
2834 """Checks for the gerrit's commit-msg hook and removes it if necessary."""
2835 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2836 if not os.path.exists(hook):
2837 return
2838 # Crude attempt to distinguish Gerrit Codereview hook from a potentially
2839 # custom developer-made one.
2840 data = gclient_utils.FileRead(hook)
2841 if not ('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2842 return
2843 print('WARNING: You have Gerrit commit-msg hook installed.\n'
2844 'It is not necessary for uploading with git cl in squash mode, '
2845 'and may interfere with it in subtle ways.\n'
2846 'We recommend you remove the commit-msg hook.')
2847 if offer_removal:
2848 if ask_for_explicit_yes('Do you want to remove it now?'):
2849 gclient_utils.rm_file_or_tree(hook)
2850 print('Gerrit commit-msg hook removed.')
2851 else:
2852 print('OK, will keep Gerrit commit-msg hook in place.')
2853
2854 def _CleanUpOldTraces(self):
2855 """Keep only the last |MAX_TRACES| traces."""
2856 try:
2857 traces = sorted([
2858 os.path.join(TRACES_DIR, f) for f in os.listdir(TRACES_DIR)
2859 if (os.path.isfile(os.path.join(TRACES_DIR, f))
2860 and not f.startswith('tmp'))
2861 ])
2862 traces_to_delete = traces[:-MAX_TRACES]
2863 for trace in traces_to_delete:
2864 os.remove(trace)
2865 except OSError:
2866 print('WARNING: Failed to remove old git traces from\n'
2867 ' %s'
2868 'Consider removing them manually.' % TRACES_DIR)
2869
2870 def _WriteGitPushTraces(self, trace_name, traces_dir, git_push_metadata):
2871 """Zip and write the git push traces stored in traces_dir."""
2872 gclient_utils.safe_makedirs(TRACES_DIR)
2873 traces_zip = trace_name + '-traces'
2874 traces_readme = trace_name + '-README'
2875 # Create a temporary dir to store git config and gitcookies in. It will
2876 # be compressed and stored next to the traces.
2877 git_info_dir = tempfile.mkdtemp()
2878 git_info_zip = trace_name + '-git-info'
2879
2880 git_push_metadata['now'] = datetime_now().strftime(
2881 '%Y-%m-%dT%H:%M:%S.%f')
2882
2883 git_push_metadata['trace_name'] = trace_name
2884 gclient_utils.FileWrite(traces_readme,
2885 TRACES_README_FORMAT % git_push_metadata)
2886
2887 # Keep only the first 6 characters of the git hashes on the packet
2888 # trace. This greatly decreases size after compression.
2889 packet_traces = os.path.join(traces_dir, 'trace-packet')
2890 if os.path.isfile(packet_traces):
2891 contents = gclient_utils.FileRead(packet_traces)
2892 gclient_utils.FileWrite(packet_traces,
2893 GIT_HASH_RE.sub(r'\1', contents))
2894 shutil.make_archive(traces_zip, 'zip', traces_dir)
2895
2896 # Collect and compress the git config and gitcookies.
2897 git_config = RunGit(['config', '-l'])
2898 gclient_utils.FileWrite(os.path.join(git_info_dir, 'git-config'),
2899 git_config)
2900
2901 cookie_auth = gerrit_util.Authenticator.get()
2902 if isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
2903 gitcookies_path = cookie_auth.get_gitcookies_path()
2904 if os.path.isfile(gitcookies_path):
2905 gitcookies = gclient_utils.FileRead(gitcookies_path)
2906 gclient_utils.FileWrite(
2907 os.path.join(git_info_dir, 'gitcookies'),
2908 GITCOOKIES_REDACT_RE.sub('REDACTED', gitcookies))
2909 shutil.make_archive(git_info_zip, 'zip', git_info_dir)
2910
2911 gclient_utils.rmtree(git_info_dir)
2912
2913 def _RunGitPushWithTraces(self,
2914 refspec,
2915 refspec_opts,
2916 git_push_metadata,
2917 git_push_options=None):
2918 """Run git push and collect the traces resulting from the execution."""
2919 # Create a temporary directory to store traces in. Traces will be
2920 # compressed and stored in a 'traces' dir inside depot_tools.
2921 traces_dir = tempfile.mkdtemp()
2922 trace_name = os.path.join(TRACES_DIR,
2923 datetime_now().strftime('%Y%m%dT%H%M%S.%f'))
2924
2925 env = os.environ.copy()
2926 env['GIT_REDACT_COOKIES'] = 'o,SSO,GSSO_Uberproxy'
2927 env['GIT_TR2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
2928 env['GIT_TRACE2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
2929 env['GIT_TRACE_CURL'] = os.path.join(traces_dir, 'trace-curl')
2930 env['GIT_TRACE_CURL_NO_DATA'] = '1'
2931 env['GIT_TRACE_PACKET'] = os.path.join(traces_dir, 'trace-packet')
2932
2933 push_returncode = 0
2934 before_push = time_time()
2935 try:
2936 remote_url = self.GetRemoteUrl()
2937 push_cmd = ['git', 'push', remote_url, refspec]
2938 if git_push_options:
2939 for opt in git_push_options:
2940 push_cmd.extend(['-o', opt])
2941
2942 push_stdout = gclient_utils.CheckCallAndFilter(
2943 push_cmd,
2944 env=env,
2945 print_stdout=True,
2946 # Flush after every line: useful for seeing progress when
2947 # running as recipe.
2948 filter_fn=lambda _: sys.stdout.flush())
2949 push_stdout = push_stdout.decode('utf-8', 'replace')
2950 except subprocess2.CalledProcessError as e:
2951 push_returncode = e.returncode
2952 if 'blocked keyword' in str(e.stdout) or 'banned word' in str(
2953 e.stdout):
2954 raise GitPushError(
2955 'Failed to create a change, very likely due to blocked keyword. '
2956 'Please examine output above for the reason of the failure.\n'
2957 'If this is a false positive, you can try to bypass blocked '
2958 'keyword by using push option '
2959 '-o banned-words~skip, e.g.:\n'
2960 'git cl upload -o banned-words~skip\n\n'
2961 'If git-cl is not working correctly, file a bug under the '
2962 'Infra>SDK component.')
2963 if 'git push -o nokeycheck' in str(e.stdout):
2964 raise GitPushError(
2965 'Failed to create a change, very likely due to a private key being '
2966 'detected. Please examine output above for the reason of the '
2967 'failure.\n'
2968 'If this is a false positive, you can try to bypass private key '
2969 'detection by using push option '
2970 '-o nokeycheck, e.g.:\n'
2971 'git cl upload -o nokeycheck\n\n'
2972 'If git-cl is not working correctly, file a bug under the '
2973 'Infra>SDK component.')
2974
2975 raise GitPushError(
2976 'Failed to create a change. Please examine output above for the '
2977 'reason of the failure.\n'
2978 'For emergencies, Googlers can escalate to '
2979 'go/gob-support or go/notify#gob\n'
2980 'Hint: run command below to diagnose common Git/Gerrit '
2981 'credential problems:\n'
2982 ' git cl creds-check\n'
2983 '\n'
2984 'If git-cl is not working correctly, file a bug under the Infra>SDK '
2985 'component including the files below.\n'
2986 'Review the files before upload, since they might contain sensitive '
2987 'information.\n'
2988 'Set the Restrict-View-Google label so that they are not publicly '
2989 'accessible.\n' + TRACES_MESSAGE % {'trace_name': trace_name})
2990 finally:
2991 execution_time = time_time() - before_push
2992 metrics.collector.add_repeated(
2993 'sub_commands', {
2994 'command':
2995 'git push',
2996 'execution_time':
2997 execution_time,
2998 'exit_code':
2999 push_returncode,
3000 'arguments':
3001 metrics_utils.extract_known_subcommand_args(refspec_opts),
3002 })
3003
3004 git_push_metadata['execution_time'] = execution_time
3005 git_push_metadata['exit_code'] = push_returncode
3006 self._WriteGitPushTraces(trace_name, traces_dir, git_push_metadata)
3007
3008 self._CleanUpOldTraces()
3009 gclient_utils.rmtree(traces_dir)
3010
3011 return push_stdout
3012
3013 def CMDUploadChange(self, options, git_diff_args, custom_cl_base,
3014 change_desc):
3015 """Upload the current branch to Gerrit, retry if new remote HEAD is
3016 found. options and change_desc may be mutated."""
3017 remote, remote_branch = self.GetRemoteBranch()
3018 branch = GetTargetRef(remote, remote_branch, options.target_branch)
3019
3020 try:
3021 return self._CMDUploadChange(options, git_diff_args, custom_cl_base,
3022 change_desc, branch)
3023 except GitPushError as e:
3024 # Repository might be in the middle of transition to main branch as
3025 # default, and uploads to old default might be blocked.
3026 if remote_branch not in [DEFAULT_OLD_BRANCH, DEFAULT_NEW_BRANCH]:
3027 DieWithError(str(e), change_desc)
3028
3029 project_head = gerrit_util.GetProjectHead(self._gerrit_host,
3030 self.GetGerritProject())
3031 if project_head == branch:
3032 DieWithError(str(e), change_desc)
3033 branch = project_head
3034
3035 print("WARNING: Fetching remote state and retrying upload to default "
3036 "branch...")
3037 RunGit(['fetch', '--prune', remote])
3038 options.edit_description = False
3039 options.force = True
3040 try:
3041 self._CMDUploadChange(options, git_diff_args, custom_cl_base,
3042 change_desc, branch)
3043 except GitPushError as e:
3044 DieWithError(str(e), change_desc)
3045
3046 def _CMDUploadChange(self, options, git_diff_args, custom_cl_base,
3047 change_desc, branch):
3048 """Upload the current branch to Gerrit."""
3049 if options.squash:
3050 Changelist._GerritCommitMsgHookCheck(
3051 offer_removal=not options.force)
3052 external_parent = None
3053 if self.GetIssue():
3054 # User requested to change description
3055 if options.edit_description:
3056 change_desc.prompt()
3057 change_detail = self._GetChangeDetail(['CURRENT_REVISION'])
3058 change_id = change_detail['change_id']
3059 change_desc.ensure_change_id(change_id)
3060
3061 # Check if changes outside of this workspace have been uploaded.
3062 current_rev = change_detail['current_revision']
3063 last_uploaded_rev = self._GitGetBranchConfigValue(
3064 GERRIT_SQUASH_HASH_CONFIG_KEY)
3065 if last_uploaded_rev and current_rev != last_uploaded_rev:
3066 external_parent = self._UpdateWithExternalChanges()
3067 else: # if not self.GetIssue()
3068 if not options.force and not options.message_file:
3069 change_desc.prompt()
3070 change_ids = git_footers.get_footer_change_id(
3071 change_desc.description)
3072 if len(change_ids) == 1:
3073 change_id = change_ids[0]
3074 else:
3075 change_id = GenerateGerritChangeId(change_desc.description)
3076 change_desc.ensure_change_id(change_id)
3077
3078 if options.preserve_tryjobs:
3079 change_desc.set_preserve_tryjobs()
3080
3081 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
3082 parent = external_parent or self._ComputeParent(
3083 remote, upstream_branch, custom_cl_base, options.force,
3084 change_desc)
3085 tree = RunGit(['rev-parse', 'HEAD:']).strip()
3086 with gclient_utils.temporary_file() as desc_tempfile:
3087 gclient_utils.FileWrite(desc_tempfile, change_desc.description)
3088 ref_to_push = RunGit(
3089 ['commit-tree', tree, '-p', parent, '-F',
3090 desc_tempfile]).strip()
3091 else: # if not options.squash
3092 if options.no_add_changeid:
3093 pass
3094 else: # adding Change-Ids is okay.
3095 if not git_footers.get_footer_change_id(
3096 change_desc.description):
3097 DownloadGerritHook(False)
3098 change_desc.set_description(
3099 self._AddChangeIdToCommitMessage(
3100 change_desc.description, git_diff_args))
3101 ref_to_push = 'HEAD'
3102 # For no-squash mode, we assume the remote called "origin" is the
3103 # one we want. It is not worthwhile to support different workflows
3104 # for no-squash mode.
3105 parent = 'origin/%s' % branch
3106 # attempt to extract the changeid from the current description
3107 # fail informatively if not possible.
3108 change_id_candidates = git_footers.get_footer_change_id(
3109 change_desc.description)
3110 if not change_id_candidates:
3111 DieWithError("Unable to extract change-id from message.")
3112 change_id = change_id_candidates[0]
3113
3114 SaveDescriptionBackup(change_desc)
3115 commits = RunGitSilent(['rev-list',
3116 '%s..%s' % (parent, ref_to_push)]).splitlines()
3117 if len(commits) > 1:
3118 print(
3119 'WARNING: This will upload %d commits. Run the following command '
3120 'to see which commits will be uploaded: ' % len(commits))
3121 print('git log %s..%s' % (parent, ref_to_push))
3122 print('You can also use `git squash-branch` to squash these into a '
3123 'single commit.')
3124 confirm_or_exit(action='upload')
3125
3126 reviewers = sorted(change_desc.get_reviewers())
3127 cc = []
3128 # Add default, watchlist, presubmit ccs if this is the initial upload
3129 # and CL is not private and auto-ccing has not been disabled.
3130 if not options.private and not options.no_autocc and not self.GetIssue(
3131 ):
3132 cc = self.GetCCList().split(',')
3133 if len(cc) > 100:
3134 lsc = ('https://chromium.googlesource.com/chromium/src/+/HEAD/docs/'
3135 'process/lsc/lsc_workflow.md')
3136 print('WARNING: This will auto-CC %s users.' % len(cc))
3137 print('LSC may be more appropriate: %s' % lsc)
3138 print('You can also use the --no-autocc flag to disable auto-CC.')
3139 confirm_or_exit(action='continue')
3140 # Add cc's from the --cc flag.
3141 if options.cc:
3142 cc.extend(options.cc)
3143 cc = [email.strip() for email in cc if email.strip()]
3144 if change_desc.get_cced():
3145 cc.extend(change_desc.get_cced())
3146 if self.GetGerritHost() == 'chromium-review.googlesource.com':
3147 valid_accounts = set(reviewers + cc)
3148 # TODO(crbug/877717): relax this for all hosts.
3149 else:
3150 valid_accounts = gerrit_util.ValidAccounts(self.GetGerritHost(),
3151 reviewers + cc)
3152 logging.info('accounts %s are recognized, %s invalid',
3153 sorted(valid_accounts),
3154 set(reviewers + cc).difference(set(valid_accounts)))
3155
3156 # Extra options that can be specified at push time. Doc:
3157 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
3158 refspec_opts = self._GetRefSpecOptions(options, change_desc)
3159
3160 for r in sorted(reviewers):
3161 if r in valid_accounts:
3162 refspec_opts.append('r=%s' % r)
3163 reviewers.remove(r)
3164 else:
3165 # TODO(tandrii): this should probably be a hard failure.
3166 print(
3167 'WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
3168 % r)
3169 for c in sorted(cc):
3170 # refspec option will be rejected if cc doesn't correspond to an
3171 # account, even though REST call to add such arbitrary cc may
3172 # succeed.
3173 if c in valid_accounts:
3174 refspec_opts.append('cc=%s' % c)
3175 cc.remove(c)
3176
3177 refspec_suffix = ''
3178 if refspec_opts:
3179 refspec_suffix = '%' + ','.join(refspec_opts)
3180 assert ' ' not in refspec_suffix, (
3181 'spaces not allowed in refspec: "%s"' % refspec_suffix)
3182 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
3183
3184 git_push_metadata = {
3185 'gerrit_host': self.GetGerritHost(),
3186 'title': options.title or '<untitled>',
3187 'change_id': change_id,
3188 'description': change_desc.description,
3189 }
3190
3191 # Gerrit may or may not update fast enough to return the correct
3192 # patchset number after we push. Get the pre-upload patchset and
3193 # increment later.
3194 latest_ps = self.GetMostRecentPatchset(update=False) or 0
3195
3196 push_stdout = self._RunGitPushWithTraces(refspec, refspec_opts,
3197 git_push_metadata,
3198 options.push_options)
3199
3200 if options.squash:
3201 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
3202 change_numbers = [
3203 m.group(1) for m in map(regex.match, push_stdout.splitlines())
3204 if m
3205 ]
3206 if len(change_numbers) != 1:
3207 DieWithError((
3208 'Created|Updated %d issues on Gerrit, but only 1 expected.\n'
3209 'Change-Id: %s') % (len(change_numbers), change_id),
3210 change_desc)
3211 self.SetIssue(change_numbers[0])
3212 self.SetPatchset(latest_ps + 1)
3213 self._GitSetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY,
3214 ref_to_push)
3215
3216 if self.GetIssue() and (reviewers or cc):
3217 # GetIssue() is not set in case of non-squash uploads according to
3218 # tests. TODO(crbug.com/751901): non-squash uploads in git cl should
3219 # be removed.
3220 gerrit_util.AddReviewers(self.GetGerritHost(),
3221 self._GerritChangeIdentifier(),
3222 reviewers,
3223 cc,
3224 notify=bool(options.send_mail))
3225
3226 return 0
3227
3228 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
3229 change_desc):
3230 """Computes parent of the generated commit to be uploaded to Gerrit.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003231
3232 Returns revision or a ref name.
3233 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003234 if custom_cl_base:
3235 # Try to avoid creating additional unintended CLs when uploading,
3236 # unless user wants to take this risk.
3237 local_ref_of_target_remote = self.GetRemoteBranch()[1]
3238 code, _ = RunGitWithCode([
3239 'merge-base', '--is-ancestor', custom_cl_base,
3240 local_ref_of_target_remote
3241 ])
3242 if code == 1:
3243 print(
3244 '\nWARNING: Manually specified base of this CL `%s` '
3245 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
3246 'If you proceed with upload, more than 1 CL may be created by '
3247 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
3248 'If you are certain that specified base `%s` has already been '
3249 'uploaded to Gerrit as another CL, you may proceed.\n' %
3250 (custom_cl_base, local_ref_of_target_remote,
3251 custom_cl_base))
3252 if not force:
3253 confirm_or_exit(
3254 'Do you take responsibility for cleaning up potential mess '
3255 'resulting from proceeding with upload?',
3256 action='upload')
3257 return custom_cl_base
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003258
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003259 if remote != '.':
3260 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003261
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003262 # If our upstream branch is local, we base our squashed commit on its
3263 # squashed version.
3264 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
Aaron Gablef97e33d2017-03-30 15:44:27 -07003265
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003266 if upstream_branch_name == 'master':
3267 return self.GetCommonAncestorWithUpstream()
3268 if upstream_branch_name == 'main':
3269 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003270
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003271 # Check the squashed hash of the parent.
3272 # TODO(tandrii): consider checking parent change in Gerrit and using its
3273 # hash if tree hash of latest parent revision (patchset) in Gerrit
3274 # matches the tree hash of the parent branch. The upside is less likely
3275 # bogus requests to reupload parent change just because it's uploadhash
3276 # is missing, yet the downside likely exists, too (albeit unknown to me
3277 # yet).
3278 parent = scm.GIT.GetBranchConfig(settings.GetRoot(),
3279 upstream_branch_name,
3280 GERRIT_SQUASH_HASH_CONFIG_KEY)
3281 # Verify that the upstream branch has been uploaded too, otherwise
3282 # Gerrit will create additional CLs when uploading.
3283 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
3284 RunGitSilent(['rev-parse', parent + ':'])):
3285 DieWithError(
3286 '\nUpload upstream branch %s first.\n'
3287 'It is likely that this branch has been rebased since its last '
3288 'upload, so you just need to upload it again.\n'
3289 '(If you uploaded it with --no-squash, then branch dependencies '
3290 'are not supported, and you should reupload with --squash.)' %
3291 upstream_branch_name, change_desc)
3292 return parent
Aaron Gablef97e33d2017-03-30 15:44:27 -07003293
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003294 def _UpdateWithExternalChanges(self):
3295 """Updates workspace with external changes.
Gavin Mak4e5e3992022-11-14 22:40:12 +00003296
3297 Returns the commit hash that should be used as the merge base on upload.
3298 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003299 local_ps = self.GetPatchset()
3300 if local_ps is None:
3301 return
Gavin Mak4e5e3992022-11-14 22:40:12 +00003302
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003303 external_ps = self.GetMostRecentPatchset(update=False)
3304 if external_ps is None or local_ps == external_ps or \
3305 not self._IsPatchsetRangeSignificant(local_ps + 1, external_ps):
3306 return
Gavin Mak4e5e3992022-11-14 22:40:12 +00003307
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003308 num_changes = external_ps - local_ps
3309 if num_changes > 1:
3310 change_words = 'changes were'
3311 else:
3312 change_words = 'change was'
3313 print('\n%d external %s published to %s:\n' %
3314 (num_changes, change_words, self.GetIssueURL(short=True)))
Gavin Mak6f905472023-01-06 21:01:36 +00003315
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003316 # Print an overview of external changes.
3317 ps_to_commit = {}
3318 ps_to_info = {}
3319 revisions = self._GetChangeDetail(['ALL_REVISIONS'])
3320 for commit_id, revision_info in revisions.get('revisions', {}).items():
3321 ps_num = revision_info['_number']
3322 ps_to_commit[ps_num] = commit_id
3323 ps_to_info[ps_num] = revision_info
Gavin Mak6f905472023-01-06 21:01:36 +00003324
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003325 for ps in range(external_ps, local_ps, -1):
3326 commit = ps_to_commit[ps][:8]
3327 desc = ps_to_info[ps].get('description', '')
3328 print('Patchset %d [%s] %s' % (ps, commit, desc))
Gavin Mak6f905472023-01-06 21:01:36 +00003329
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003330 print('\nSee diff at: %s/%d..%d' %
3331 (self.GetIssueURL(short=True), local_ps, external_ps))
3332 print('\nUploading without applying patches will override them.')
Josip Sokcevic43ceaf02023-05-25 15:56:00 +00003333
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003334 if not ask_for_explicit_yes('Get the latest changes and apply on top?'):
3335 return
Gavin Mak4e5e3992022-11-14 22:40:12 +00003336
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003337 # Get latest Gerrit merge base. Use the first parent even if multiple
3338 # exist.
3339 external_parent = self._GetChangeCommit(
3340 revision=external_ps)['parents'][0]
3341 external_base = external_parent['commit']
Gavin Mak4e5e3992022-11-14 22:40:12 +00003342
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003343 branch = git_common.current_branch()
3344 local_base = self.GetCommonAncestorWithUpstream()
3345 if local_base != external_base:
3346 print('\nLocal merge base %s is different from Gerrit %s.\n' %
3347 (local_base, external_base))
3348 if git_common.upstream(branch):
3349 confirm_or_exit(
3350 'Can\'t apply the latest changes from Gerrit.\n'
3351 'Continue with upload and override the latest changes?')
3352 return
3353 print(
3354 'No upstream branch set. Continuing upload with Gerrit merge base.'
3355 )
Gavin Mak4e5e3992022-11-14 22:40:12 +00003356
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003357 external_parent_last_uploaded = self._GetChangeCommit(
3358 revision=local_ps)['parents'][0]
3359 external_base_last_uploaded = external_parent_last_uploaded['commit']
Josip Sokcevic2568d4c2023-05-24 03:37:42 +00003360
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003361 if external_base != external_base_last_uploaded:
3362 print('\nPatch set merge bases are different (%s, %s).\n' %
3363 (external_base_last_uploaded, external_base))
3364 confirm_or_exit(
3365 'Can\'t apply the latest changes from Gerrit.\n'
3366 'Continue with upload and override the latest changes?')
3367 return
Josip Sokcevic2568d4c2023-05-24 03:37:42 +00003368
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003369 # Fetch Gerrit's CL base if it doesn't exist locally.
3370 remote, _ = self.GetRemoteBranch()
3371 if not scm.GIT.IsValidRevision(settings.GetRoot(), external_base):
3372 RunGitSilent(['fetch', remote, external_base])
Gavin Mak4e5e3992022-11-14 22:40:12 +00003373
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003374 # Get the diff between local_ps and external_ps.
3375 print('Fetching changes...')
3376 issue = self.GetIssue()
3377 changes_ref = 'refs/changes/%02d/%d/' % (issue % 100, issue)
3378 RunGitSilent(['fetch', remote, changes_ref + str(local_ps)])
3379 last_uploaded = RunGitSilent(['rev-parse', 'FETCH_HEAD']).strip()
3380 RunGitSilent(['fetch', remote, changes_ref + str(external_ps)])
3381 latest_external = RunGitSilent(['rev-parse', 'FETCH_HEAD']).strip()
Josip Sokcevic2568d4c2023-05-24 03:37:42 +00003382
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003383 # If the commit parents are different, don't apply the diff as it very
3384 # likely contains many more changes not relevant to this CL.
3385 parents = RunGitSilent(
3386 ['rev-parse',
3387 '%s~1' % (last_uploaded),
3388 '%s~1' % (latest_external)]).strip().split()
3389 assert len(parents) == 2, 'Expected two parents.'
3390 if parents[0] != parents[1]:
3391 confirm_or_exit(
3392 'Can\'t apply the latest changes from Gerrit (parent mismatch '
3393 'between PS).\n'
3394 'Continue with upload and override the latest changes?')
3395 return
Josip Sokcevic2568d4c2023-05-24 03:37:42 +00003396
Joanna Wangbcba1782023-09-12 22:48:05 +00003397 diff = RunGitSilent([
3398 'diff', '--no-ext-diff',
3399 '%s..%s' % (last_uploaded, latest_external)
3400 ])
Gavin Mak4e5e3992022-11-14 22:40:12 +00003401
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003402 # Diff can be empty in the case of trivial rebases.
3403 if not diff:
3404 return external_base
Gavin Mak4e5e3992022-11-14 22:40:12 +00003405
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003406 # Apply the diff.
3407 with gclient_utils.temporary_file() as diff_tempfile:
3408 gclient_utils.FileWrite(diff_tempfile, diff)
3409 clean_patch = RunGitWithCode(['apply', '--check',
3410 diff_tempfile])[0] == 0
3411 RunGitSilent(['apply', '-3', '--intent-to-add', diff_tempfile])
3412 if not clean_patch:
3413 # Normally patchset is set after upload. But because we exit,
3414 # that never happens. Updating here makes sure that subsequent
3415 # uploads don't need to fetch/apply the same diff again.
3416 self.SetPatchset(external_ps)
3417 DieWithError(
3418 '\nPatch did not apply cleanly. Please resolve any '
3419 'conflicts and reupload.')
Gavin Mak4e5e3992022-11-14 22:40:12 +00003420
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003421 message = 'Incorporate external changes from '
3422 if num_changes == 1:
3423 message += 'patchset %d' % external_ps
3424 else:
3425 message += 'patchsets %d to %d' % (local_ps + 1, external_ps)
3426 RunGitSilent(['commit', '-am', message])
3427 # TODO(crbug.com/1382528): Use the previous commit's message as a
3428 # default patchset title instead of this 'Incorporate' message.
3429 return external_base
Gavin Mak4e5e3992022-11-14 22:40:12 +00003430
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003431 def _AddChangeIdToCommitMessage(self, log_desc, args):
3432 """Re-commits using the current message, assumes the commit hook is in
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003433 place.
3434 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003435 RunGit(['commit', '--amend', '-m', log_desc])
3436 new_log_desc = _create_description_from_log(args)
3437 if git_footers.get_footer_change_id(new_log_desc):
3438 print('git-cl: Added Change-Id to commit message.')
3439 return new_log_desc
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00003440
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003441 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003442
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003443 def CannotTriggerTryJobReason(self):
3444 try:
3445 data = self._GetChangeDetail()
3446 except GerritChangeNotExists:
3447 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003448
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003449 if data['status'] in ('ABANDONED', 'MERGED'):
3450 return 'CL %s is closed' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003451
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003452 def GetGerritChange(self, patchset=None):
3453 """Returns a buildbucket.v2.GerritChange message for the current issue."""
3454 host = urllib.parse.urlparse(self.GetCodereviewServer()).hostname
3455 issue = self.GetIssue()
3456 patchset = int(patchset or self.GetPatchset())
3457 data = self._GetChangeDetail(['ALL_REVISIONS'])
Edward Lemurd4d1ba42019-09-20 21:46:37 +00003458
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003459 assert host and issue and patchset, 'CL must be uploaded first'
Edward Lemurd4d1ba42019-09-20 21:46:37 +00003460
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003461 has_patchset = any(
3462 int(revision_data['_number']) == patchset
3463 for revision_data in data['revisions'].values())
3464 if not has_patchset:
3465 raise Exception('Patchset %d is not known in Gerrit change %d' %
3466 (patchset, self.GetIssue()))
Edward Lemurd4d1ba42019-09-20 21:46:37 +00003467
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003468 return {
3469 'host': host,
3470 'change': issue,
3471 'project': data['project'],
3472 'patchset': patchset,
3473 }
tandriie113dfd2016-10-11 10:20:12 -07003474
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003475 def GetIssueOwner(self):
3476 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003477
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003478 def GetReviewers(self):
3479 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
3480 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01003481
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003482
Lei Zhang8a0efc12020-08-05 19:58:45 +00003483def _get_bug_line_values(default_project_prefix, bugs):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003484 """Given default_project_prefix and comma separated list of bugs, yields bug
Lei Zhang8a0efc12020-08-05 19:58:45 +00003485 line values.
tandriif9aefb72016-07-01 09:06:51 -07003486
3487 Each bug can be either:
Lei Zhang8a0efc12020-08-05 19:58:45 +00003488 * a number, which is combined with default_project_prefix
tandriif9aefb72016-07-01 09:06:51 -07003489 * string, which is left as is.
3490
3491 This function may produce more than one line, because bugdroid expects one
3492 project per line.
3493
Lei Zhang8a0efc12020-08-05 19:58:45 +00003494 >>> list(_get_bug_line_values('v8:', '123,chromium:789'))
tandriif9aefb72016-07-01 09:06:51 -07003495 ['v8:123', 'chromium:789']
3496 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003497 default_bugs = []
3498 others = []
3499 for bug in bugs.split(','):
3500 bug = bug.strip()
3501 if bug:
3502 try:
3503 default_bugs.append(int(bug))
3504 except ValueError:
3505 others.append(bug)
tandriif9aefb72016-07-01 09:06:51 -07003506
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003507 if default_bugs:
3508 default_bugs = ','.join(map(str, default_bugs))
3509 if default_project_prefix:
3510 if not default_project_prefix.endswith(':'):
3511 default_project_prefix += ':'
3512 yield '%s%s' % (default_project_prefix, default_bugs)
3513 else:
3514 yield default_bugs
3515 for other in sorted(others):
3516 # Don't bother finding common prefixes, CLs with >2 bugs are very very
3517 # rare.
3518 yield other
tandriif9aefb72016-07-01 09:06:51 -07003519
3520
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003521def FindCodereviewSettingsFile(filename='codereview.settings'):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003522 """Finds the given file starting in the cwd and going up.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003523
3524 Only looks up to the top of the repository unless an
3525 'inherit-review-settings-ok' file exists in the root of the repository.
3526 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003527 inherit_ok_file = 'inherit-review-settings-ok'
3528 cwd = os.getcwd()
3529 root = settings.GetRoot()
3530 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3531 root = None
3532 while True:
3533 if os.path.isfile(os.path.join(cwd, filename)):
3534 return open(os.path.join(cwd, filename))
3535 if cwd == root:
3536 break
3537 parent_dir = os.path.dirname(cwd)
3538 if parent_dir == cwd:
3539 # We hit the system root directory.
3540 break
3541 cwd = parent_dir
3542 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003543
3544
3545def LoadCodereviewSettingsFromFile(fileobj):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003546 """Parses a codereview.settings file and updates hooks."""
3547 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003548
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003549 def SetProperty(name, setting, unset_error_ok=False):
3550 fullname = 'rietveld.' + name
3551 if setting in keyvals:
3552 RunGit(['config', fullname, keyvals[setting]])
3553 else:
3554 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003555
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003556 if not keyvals.get('GERRIT_HOST', False):
3557 SetProperty('server', 'CODE_REVIEW_SERVER')
3558 # Only server setting is required. Other settings can be absent.
3559 # In that case, we ignore errors raised during option deletion attempt.
3560 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
3561 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3562 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
3563 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
3564 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3565 SetProperty('cpplint-ignore-regex',
3566 'LINT_IGNORE_REGEX',
3567 unset_error_ok=True)
3568 SetProperty('run-post-upload-hook',
3569 'RUN_POST_UPLOAD_HOOK',
3570 unset_error_ok=True)
3571 SetProperty('format-full-by-default',
3572 'FORMAT_FULL_BY_DEFAULT',
3573 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003574
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003575 if 'GERRIT_HOST' in keyvals:
3576 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003577
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003578 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
3579 RunGit([
3580 'config', 'gerrit.squash-uploads', keyvals['GERRIT_SQUASH_UPLOADS']
3581 ])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003582
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003583 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
3584 RunGit([
3585 'config', 'gerrit.skip-ensure-authenticated',
3586 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']
3587 ])
tandrii@chromium.org28253532016-04-14 13:46:56 +00003588
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003589 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3590 # should be of the form
3591 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3592 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
3593 RunGit([
3594 'config', keyvals['PUSH_URL_CONFIG'], keyvals['ORIGIN_URL_CONFIG']
3595 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003596
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003597
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003598def urlretrieve(source, destination):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003599 """Downloads a network object to a local file, like urllib.urlretrieve.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003600
3601 This is necessary because urllib is broken for SSL connections via a proxy.
3602 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003603 with open(destination, 'wb') as f:
3604 f.write(urllib.request.urlopen(source).read())
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003605
3606
ukai@chromium.org712d6102013-11-27 00:52:58 +00003607def hasSheBang(fname):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003608 """Checks fname is a #! script."""
3609 with open(fname) as f:
3610 return f.read(2).startswith('#!')
ukai@chromium.org712d6102013-11-27 00:52:58 +00003611
3612
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003613def DownloadGerritHook(force):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003614 """Downloads and installs a Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003615
3616 Args:
3617 force: True to update hooks. False to install hooks if not present.
3618 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003619 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
3620 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3621 if not os.access(dst, os.X_OK):
3622 if os.path.exists(dst):
3623 if not force:
3624 return
3625 try:
3626 urlretrieve(src, dst)
3627 if not hasSheBang(dst):
3628 DieWithError('Not a script: %s\n'
3629 'You need to download from\n%s\n'
3630 'into .git/hooks/commit-msg and '
3631 'chmod +x .git/hooks/commit-msg' % (dst, src))
3632 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3633 except Exception:
3634 if os.path.exists(dst):
3635 os.remove(dst)
3636 DieWithError('\nFailed to download hooks.\n'
3637 'You need to download from\n%s\n'
3638 'into .git/hooks/commit-msg and '
3639 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003640
3641
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003642class _GitCookiesChecker(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003643 """Provides facilities for validating and suggesting fixes to .gitcookies."""
3644 def __init__(self):
3645 # Cached list of [host, identity, source], where source is either
3646 # .gitcookies or .netrc.
3647 self._all_hosts = None
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003648
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003649 def ensure_configured_gitcookies(self):
3650 """Runs checks and suggests fixes to make git use .gitcookies from default
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003651 path."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003652 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3653 configured_path = RunGitSilent(
3654 ['config', '--global', 'http.cookiefile']).strip()
3655 configured_path = os.path.expanduser(configured_path)
3656 if configured_path:
3657 self._ensure_default_gitcookies_path(configured_path, default)
3658 else:
3659 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003660
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003661 @staticmethod
3662 def _ensure_default_gitcookies_path(configured_path, default_path):
3663 assert configured_path
3664 if configured_path == default_path:
3665 print('git is already configured to use your .gitcookies from %s' %
3666 configured_path)
3667 return
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003668
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003669 print('WARNING: You have configured custom path to .gitcookies: %s\n'
3670 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3671 (configured_path, default_path))
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003672
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003673 if not os.path.exists(configured_path):
3674 print('However, your configured .gitcookies file is missing.')
3675 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3676 action='reconfigure')
3677 RunGit(['config', '--global', 'http.cookiefile', default_path])
3678 return
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003679
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003680 if os.path.exists(default_path):
3681 print('WARNING: default .gitcookies file already exists %s' %
3682 default_path)
3683 DieWithError(
3684 'Please delete %s manually and re-run git cl creds-check' %
3685 default_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003686
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003687 confirm_or_exit('Move existing .gitcookies to default location?',
3688 action='move')
3689 shutil.move(configured_path, default_path)
3690 RunGit(['config', '--global', 'http.cookiefile', default_path])
3691 print('Moved and reconfigured git to use .gitcookies from %s' %
3692 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003693
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003694 @staticmethod
3695 def _configure_gitcookies_path(default_path):
3696 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3697 if os.path.exists(netrc_path):
3698 print(
3699 'You seem to be using outdated .netrc for git credentials: %s' %
3700 netrc_path)
3701 print(
3702 'This tool will guide you through setting up recommended '
3703 '.gitcookies store for git credentials.\n'
3704 '\n'
3705 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3706 ' git config --global --unset http.cookiefile\n'
3707 ' mv %s %s.backup\n\n' % (default_path, default_path))
3708 confirm_or_exit(action='setup .gitcookies')
3709 RunGit(['config', '--global', 'http.cookiefile', default_path])
3710 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003711
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003712 def get_hosts_with_creds(self, include_netrc=False):
3713 if self._all_hosts is None:
3714 a = gerrit_util.CookiesAuthenticator()
3715 self._all_hosts = [(h, u, s) for h, u, s in itertools.chain((
3716 (h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.items()), (
3717 (h, u, '.gitcookies')
3718 for h, (u, _) in a.gitcookies.items()))
3719 if h.endswith(_GOOGLESOURCE)]
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003720
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003721 if include_netrc:
3722 return self._all_hosts
3723 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003724
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003725 def print_current_creds(self, include_netrc=False):
3726 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3727 if not hosts:
3728 print('No Git/Gerrit credentials found')
3729 return
3730 lengths = [max(map(len, (row[i] for row in hosts))) for i in range(3)]
3731 header = [('Host', 'User', 'Which file'), ['=' * l for l in lengths]]
3732 for row in (header + hosts):
3733 print('\t'.join((('%%+%ds' % l) % s) for l, s in zip(lengths, row)))
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003734
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003735 @staticmethod
3736 def _parse_identity(identity):
3737 """Parses identity "git-<username>.domain" into <username> and domain."""
3738 # Special case: usernames that contain ".", which are generally not
3739 # distinguishable from sub-domains. But we do know typical domains:
3740 if identity.endswith('.chromium.org'):
3741 domain = 'chromium.org'
3742 username = identity[:-len('.chromium.org')]
3743 else:
3744 username, domain = identity.split('.', 1)
3745 if username.startswith('git-'):
3746 username = username[len('git-'):]
3747 return username, domain
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003748
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003749 def has_generic_host(self):
3750 """Returns whether generic .googlesource.com has been configured.
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003751
3752 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3753 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003754 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3755 if host == '.' + _GOOGLESOURCE:
3756 return True
3757 return False
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003758
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003759 def _get_git_gerrit_identity_pairs(self):
3760 """Returns map from canonic host to pair of identities (Git, Gerrit).
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003761
3762 One of identities might be None, meaning not configured.
3763 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003764 host_to_identity_pairs = {}
3765 for host, identity, _ in self.get_hosts_with_creds():
3766 canonical = _canonical_git_googlesource_host(host)
3767 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3768 idx = 0 if canonical == host else 1
3769 pair[idx] = identity
3770 return host_to_identity_pairs
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003771
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003772 def get_partially_configured_hosts(self):
3773 return set(
3774 (host if i1 else _canonical_gerrit_googlesource_host(host))
3775 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().items()
3776 if None in (i1, i2) and host != '.' + _GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003777
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003778 def get_conflicting_hosts(self):
3779 return set(
3780 host
3781 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().items()
3782 if None not in (i1, i2) and i1 != i2)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003783
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003784 def get_duplicated_hosts(self):
3785 counters = collections.Counter(
3786 h for h, _, _ in self.get_hosts_with_creds())
3787 return set(host for host, count in counters.items() if count > 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003788
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003789 @staticmethod
3790 def _format_hosts(hosts, extra_column_func=None):
3791 hosts = sorted(hosts)
3792 assert hosts
3793 if extra_column_func is None:
3794 extras = [''] * len(hosts)
3795 else:
3796 extras = [extra_column_func(host) for host in hosts]
3797 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len,
3798 extras)))
3799 lines = []
3800 for he in zip(hosts, extras):
3801 lines.append(tmpl % he)
3802 return lines
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003803
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003804 def _find_problems(self):
3805 if self.has_generic_host():
3806 yield ('.googlesource.com wildcard record detected', [
3807 'Chrome Infrastructure team recommends to list full host names '
3808 'explicitly.'
3809 ], None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003810
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003811 dups = self.get_duplicated_hosts()
3812 if dups:
3813 yield ('The following hosts were defined twice',
3814 self._format_hosts(dups), None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003815
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003816 partial = self.get_partially_configured_hosts()
3817 if partial:
3818 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3819 'These hosts are missing',
3820 self._format_hosts(
3821 partial, lambda host: 'but %s defined' %
3822 _get_counterpart_host(host)), partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003823
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003824 conflicting = self.get_conflicting_hosts()
3825 if conflicting:
3826 yield (
3827 'The following Git hosts have differing credentials from their '
3828 'Gerrit counterparts',
3829 self._format_hosts(
3830 conflicting, lambda host: '%s vs %s' % tuple(
3831 self._get_git_gerrit_identity_pairs()[host])),
3832 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003833
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003834 def find_and_report_problems(self):
3835 """Returns True if there was at least one problem, else False."""
3836 found = False
3837 bad_hosts = set()
3838 for title, sublines, hosts in self._find_problems():
3839 if not found:
3840 found = True
3841 print('\n\n.gitcookies problem report:\n')
3842 bad_hosts.update(hosts or [])
3843 print(' %s%s' % (title, (':' if sublines else '')))
3844 if sublines:
3845 print()
3846 print(' %s' % '\n '.join(sublines))
3847 print()
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003848
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003849 if bad_hosts:
3850 assert found
3851 print(
3852 ' You can manually remove corresponding lines in your %s file and '
3853 'visit the following URLs with correct account to generate '
3854 'correct credential lines:\n' %
3855 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
3856 print(' %s' % '\n '.join(
3857 sorted(
3858 set(gerrit_util.CookiesAuthenticator().get_new_password_url(
3859 _canonical_git_googlesource_host(host))
3860 for host in bad_hosts))))
3861 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003862
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003863
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003864@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003865def CMDcreds_check(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003866 """Checks credentials and suggests changes."""
3867 _, _ = parser.parse_args(args)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003868
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003869 # Code below checks .gitcookies. Abort if using something else.
3870 authn = gerrit_util.Authenticator.get()
3871 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
3872 message = (
3873 'This command is not designed for bot environment. It checks '
3874 '~/.gitcookies file not generally used on bots.')
3875 # TODO(crbug.com/1059384): Automatically detect when running on
3876 # cloudtop.
3877 if isinstance(authn, gerrit_util.GceAuthenticator):
3878 message += (
3879 '\n'
3880 'If you need to run this on GCE or a cloudtop instance, '
3881 'export SKIP_GCE_AUTH_FOR_GIT=1 in your env.')
3882 DieWithError(message)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003883
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003884 checker = _GitCookiesChecker()
3885 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003886
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003887 print('Your .netrc and .gitcookies have credentials for these hosts:')
3888 checker.print_current_creds(include_netrc=True)
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003889
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003890 if not checker.find_and_report_problems():
3891 print('\nNo problems detected in your .gitcookies file.')
3892 return 0
3893 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003894
3895
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003896@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003897def CMDbaseurl(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003898 """Gets or sets base-url for this branch."""
3899 _, args = parser.parse_args(args)
3900 branchref = scm.GIT.GetBranchRef(settings.GetRoot())
3901 branch = scm.GIT.ShortBranchName(branchref)
3902 if not args:
3903 print('Current base-url:')
3904 return RunGit(['config', 'branch.%s.base-url' % branch],
3905 error_ok=False).strip()
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00003906
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003907 print('Setting base-url to %s' % args[0])
3908 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3909 error_ok=False).strip()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003910
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003911
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003912def color_for_status(status):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003913 """Maps a Changelist status to color, for CMDstatus and other tools."""
3914 BOLD = '\033[1m'
3915 return {
3916 'unsent': BOLD + Fore.YELLOW,
3917 'waiting': BOLD + Fore.RED,
3918 'reply': BOLD + Fore.YELLOW,
3919 'not lgtm': BOLD + Fore.RED,
3920 'lgtm': BOLD + Fore.GREEN,
3921 'commit': BOLD + Fore.MAGENTA,
3922 'closed': BOLD + Fore.CYAN,
3923 'error': BOLD + Fore.WHITE,
3924 }.get(status, Fore.WHITE)
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003925
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003926
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003927def get_cl_statuses(changes, fine_grained, max_processes=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003928 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003929
3930 If fine_grained is true, this will fetch CL statuses from the server.
3931 Otherwise, simply indicate if there's a matching url for the given branches.
3932
3933 If max_processes is specified, it is used as the maximum number of processes
3934 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3935 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003936
3937 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003938 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003939 if not changes:
3940 return
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003941
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003942 if not fine_grained:
3943 # Fast path which doesn't involve querying codereview servers.
3944 # Do not use get_approving_reviewers(), since it requires an HTTP
3945 # request.
3946 for cl in changes:
3947 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
3948 return
3949
3950 # First, sort out authentication issues.
3951 logging.debug('ensuring credentials exist')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003952 for cl in changes:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003953 cl.EnsureAuthenticated(force=False, refresh=True)
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003954
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003955 def fetch(cl):
3956 try:
3957 return (cl, cl.GetStatus())
3958 except:
3959 # See http://crbug.com/629863.
3960 logging.exception('failed to fetch status for cl %s:',
3961 cl.GetIssue())
3962 raise
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003963
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003964 threads_count = len(changes)
3965 if max_processes:
3966 threads_count = max(1, min(threads_count, max_processes))
3967 logging.debug('querying %d CLs using %d threads', len(changes),
3968 threads_count)
3969
3970 pool = multiprocessing.pool.ThreadPool(threads_count)
3971 fetched_cls = set()
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003972 try:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003973 it = pool.imap_unordered(fetch, changes).__iter__()
3974 while True:
3975 try:
3976 cl, status = it.next(timeout=5)
3977 except (multiprocessing.TimeoutError, StopIteration):
3978 break
3979 fetched_cls.add(cl)
3980 yield cl, status
3981 finally:
3982 pool.close()
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003983
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003984 # Add any branches that failed to fetch.
3985 for cl in set(changes) - fetched_cls:
3986 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003987
rmistry@google.com2dd99862015-06-22 12:22:18 +00003988
Jose Lopes3863fc52020-04-07 17:00:25 +00003989def upload_branch_deps(cl, args, force=False):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003990 """Uploads CLs of local branches that are dependents of the current branch.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003991
3992 If the local branch dependency tree looks like:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003993
3994 test1 -> test2.1 -> test3.1
3995 -> test3.2
3996 -> test2.2 -> test3.3
rmistry@google.com2dd99862015-06-22 12:22:18 +00003997
3998 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3999 run on the dependent branches in this order:
4000 test2.1, test3.1, test3.2, test2.2, test3.3
4001
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004002 Note: This function does not rebase your local dependent branches. Use it
4003 when you make a change to the parent branch that will not conflict
4004 with its dependent branches, and you would like their dependencies
4005 updated in Rietveld.
rmistry@google.com2dd99862015-06-22 12:22:18 +00004006 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004007 if git_common.is_dirty_git_tree('upload-branch-deps'):
4008 return 1
rmistry@google.com2dd99862015-06-22 12:22:18 +00004009
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004010 root_branch = cl.GetBranch()
4011 if root_branch is None:
4012 DieWithError('Can\'t find dependent branches from detached HEAD state. '
4013 'Get on a branch!')
4014 if not cl.GetIssue():
4015 DieWithError(
4016 'Current branch does not have an uploaded CL. We cannot set '
4017 'patchset dependencies without an uploaded CL.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004018
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004019 branches = RunGit([
4020 'for-each-ref', '--format=%(refname:short) %(upstream:short)',
4021 'refs/heads'
4022 ])
4023 if not branches:
4024 print('No local branches found.')
4025 return 0
rmistry@google.com2dd99862015-06-22 12:22:18 +00004026
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004027 # Create a dictionary of all local branches to the branches that are
4028 # dependent on it.
4029 tracked_to_dependents = collections.defaultdict(list)
4030 for b in branches.splitlines():
4031 tokens = b.split()
4032 if len(tokens) == 2:
4033 branch_name, tracked = tokens
4034 tracked_to_dependents[tracked].append(branch_name)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004035
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004036 print()
4037 print('The dependent local branches of %s are:' % root_branch)
4038 dependents = []
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004039
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004040 def traverse_dependents_preorder(branch, padding=''):
4041 dependents_to_process = tracked_to_dependents.get(branch, [])
4042 padding += ' '
4043 for dependent in dependents_to_process:
4044 print('%s%s' % (padding, dependent))
4045 dependents.append(dependent)
4046 traverse_dependents_preorder(dependent, padding)
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004047
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004048 traverse_dependents_preorder(root_branch)
4049 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004050
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004051 if not dependents:
4052 print('There are no dependent local branches for %s' % root_branch)
4053 return 0
rmistry@google.com2dd99862015-06-22 12:22:18 +00004054
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004055 # Record all dependents that failed to upload.
4056 failures = {}
4057 # Go through all dependents, checkout the branch and upload.
4058 try:
4059 for dependent_branch in dependents:
4060 print()
4061 print('--------------------------------------')
4062 print('Running "git cl upload" from %s:' % dependent_branch)
4063 RunGit(['checkout', '-q', dependent_branch])
4064 print()
4065 try:
4066 if CMDupload(OptionParser(), args) != 0:
4067 print('Upload failed for %s!' % dependent_branch)
4068 failures[dependent_branch] = 1
4069 except: # pylint: disable=bare-except
4070 failures[dependent_branch] = 1
4071 print()
4072 finally:
4073 # Swap back to the original root branch.
4074 RunGit(['checkout', '-q', root_branch])
4075
4076 print()
4077 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004078 for dependent_branch in dependents:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004079 upload_status = 'failed' if failures.get(
4080 dependent_branch) else 'succeeded'
4081 print(' %s : %s' % (dependent_branch, upload_status))
4082 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004083
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004084 return 0
rmistry@google.com2dd99862015-06-22 12:22:18 +00004085
4086
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00004087def GetArchiveTagForBranch(issue_num, branch_name, existing_tags, pattern):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004088 """Given a proposed tag name, returns a tag name that is guaranteed to be
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00004089 unique. If 'foo' is proposed but already exists, then 'foo-2' is used,
4090 or 'foo-3', and so on."""
4091
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004092 proposed_tag = pattern.format(**{'issue': issue_num, 'branch': branch_name})
4093 for suffix_num in itertools.count(1):
4094 if suffix_num == 1:
4095 to_check = proposed_tag
4096 else:
4097 to_check = '%s-%d' % (proposed_tag, suffix_num)
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00004098
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004099 if to_check not in existing_tags:
4100 return to_check
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00004101
4102
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004103@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07004104def CMDarchive(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004105 """Archives and deletes branches associated with closed changelists."""
4106 parser.add_option(
4107 '-j',
4108 '--maxjobs',
4109 action='store',
4110 type=int,
4111 help='The maximum number of jobs to use when retrieving review status.')
4112 parser.add_option('-f',
4113 '--force',
4114 action='store_true',
4115 help='Bypasses the confirmation prompt.')
4116 parser.add_option('-d',
4117 '--dry-run',
4118 action='store_true',
4119 help='Skip the branch tagging and removal steps.')
4120 parser.add_option('-t',
4121 '--notags',
4122 action='store_true',
4123 help='Do not tag archived branches. '
4124 'Note: local commit history may be lost.')
4125 parser.add_option('-p',
4126 '--pattern',
4127 default='git-cl-archived-{issue}-{branch}',
4128 help='Format string for archive tags. '
4129 'E.g. \'archived-{issue}-{branch}\'.')
kmarshall3bff56b2016-06-06 18:31:47 -07004130
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004131 options, args = parser.parse_args(args)
4132 if args:
4133 parser.error('Unsupported args: %s' % ' '.join(args))
kmarshall3bff56b2016-06-06 18:31:47 -07004134
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004135 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4136 if not branches:
4137 return 0
4138
4139 tags = RunGit(['for-each-ref', '--format=%(refname)', 'refs/tags'
4140 ]).splitlines() or []
4141 tags = [t.split('/')[-1] for t in tags]
4142
4143 print('Finding all branches associated with closed issues...')
4144 changes = [Changelist(branchref=b) for b in branches.splitlines()]
4145 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4146 statuses = get_cl_statuses(changes,
4147 fine_grained=True,
4148 max_processes=options.maxjobs)
4149 proposal = [(cl.GetBranch(),
4150 GetArchiveTagForBranch(cl.GetIssue(), cl.GetBranch(), tags,
4151 options.pattern))
4152 for cl, status in statuses
4153 if status in ('closed', 'rietveld-not-supported')]
4154 proposal.sort()
4155
4156 if not proposal:
4157 print('No branches with closed codereview issues found.')
4158 return 0
4159
4160 current_branch = scm.GIT.GetBranch(settings.GetRoot())
4161
4162 print('\nBranches with closed issues that will be archived:\n')
4163 if options.notags:
4164 for next_item in proposal:
4165 print(' ' + next_item[0])
4166 else:
4167 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4168 for next_item in proposal:
4169 print('%*s %s' % (alignment, next_item[0], next_item[1]))
4170
4171 # Quit now on precondition failure or if instructed by the user, either
4172 # via an interactive prompt or by command line flags.
4173 if options.dry_run:
4174 print('\nNo changes were made (dry run).\n')
4175 return 0
4176
4177 if any(branch == current_branch for branch, _ in proposal):
4178 print('You are currently on a branch \'%s\' which is associated with a '
4179 'closed codereview issue, so archive cannot proceed. Please '
4180 'checkout another branch and run this command again.' %
4181 current_branch)
4182 return 1
4183
4184 if not options.force:
4185 answer = gclient_utils.AskForData(
4186 '\nProceed with deletion (Y/n)? ').lower()
4187 if answer not in ('y', ''):
4188 print('Aborted.')
4189 return 1
4190
4191 for branch, tagname in proposal:
4192 if not options.notags:
4193 RunGit(['tag', tagname, branch])
4194
4195 if RunGitWithCode(['branch', '-D', branch])[0] != 0:
4196 # Clean up the tag if we failed to delete the branch.
4197 RunGit(['tag', '-d', tagname])
4198
4199 print('\nJob\'s done!')
4200
kmarshall3bff56b2016-06-06 18:31:47 -07004201 return 0
4202
kmarshall3bff56b2016-06-06 18:31:47 -07004203
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004204@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004205def CMDstatus(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004206 """Show status of changelists.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004207
4208 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004209 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004210 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004211 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004212 - Red 'not LGTM'ed
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004213 - Magenta in the CQ
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004214 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004215 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004216
4217 Also see 'git cl comments'.
4218 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004219 parser.add_option('--no-branch-color',
4220 action='store_true',
4221 help='Disable colorized branch names')
4222 parser.add_option(
4223 '--field', help='print only specific field (desc|id|patch|status|url)')
4224 parser.add_option('-f',
4225 '--fast',
4226 action='store_true',
4227 help='Do not retrieve review status')
4228 parser.add_option(
4229 '-j',
4230 '--maxjobs',
4231 action='store',
4232 type=int,
4233 help='The maximum number of jobs to use when retrieving review status')
4234 parser.add_option(
4235 '-i',
4236 '--issue',
4237 type=int,
4238 help='Operate on this issue instead of the current branch\'s implicit '
4239 'issue. Requires --field to be set.')
4240 parser.add_option('-d',
4241 '--date-order',
4242 action='store_true',
4243 help='Order branches by committer date.')
4244 options, args = parser.parse_args(args)
4245 if args:
4246 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004247
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004248 if options.issue is not None and not options.field:
4249 parser.error('--field must be given when --issue is set.')
iannucci3c972b92016-08-17 13:24:10 -07004250
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004251 if options.field:
4252 cl = Changelist(issue=options.issue)
4253 if options.field.startswith('desc'):
4254 if cl.GetIssue():
4255 print(cl.FetchDescription())
4256 elif options.field == 'id':
4257 issueid = cl.GetIssue()
4258 if issueid:
4259 print(issueid)
4260 elif options.field == 'patch':
4261 patchset = cl.GetMostRecentPatchset()
4262 if patchset:
4263 print(patchset)
4264 elif options.field == 'status':
4265 print(cl.GetStatus())
4266 elif options.field == 'url':
4267 url = cl.GetIssueURL()
4268 if url:
4269 print(url)
4270 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004271
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004272 branches = RunGit([
4273 'for-each-ref', '--format=%(refname) %(committerdate:unix)',
4274 'refs/heads'
4275 ])
4276 if not branches:
4277 print('No local branch found.')
4278 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004279
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004280 changes = [
4281 Changelist(branchref=b, commit_date=ct)
4282 for b, ct in map(lambda line: line.split(' '), branches.splitlines())
4283 ]
4284 print('Branches associated with reviews:')
4285 output = get_cl_statuses(changes,
4286 fine_grained=not options.fast,
4287 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004288
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004289 current_branch = scm.GIT.GetBranch(settings.GetRoot())
Daniel McArdlea23bf592019-02-12 00:25:12 +00004290
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004291 def FormatBranchName(branch, colorize=False):
4292 """Simulates 'git branch' behavior. Colorizes and prefixes branch name with
Daniel McArdlea23bf592019-02-12 00:25:12 +00004293 an asterisk when it is the current branch."""
4294
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004295 asterisk = ""
4296 color = Fore.RESET
4297 if branch == current_branch:
4298 asterisk = "* "
4299 color = Fore.GREEN
4300 branch_name = scm.GIT.ShortBranchName(branch)
Daniel McArdlea23bf592019-02-12 00:25:12 +00004301
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004302 if colorize:
4303 return asterisk + color + branch_name + Fore.RESET
4304 return asterisk + branch_name
Daniel McArdle452a49f2019-02-14 17:28:31 +00004305
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004306 branch_statuses = {}
Daniel McArdlea23bf592019-02-12 00:25:12 +00004307
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004308 alignment = max(5,
4309 max(len(FormatBranchName(c.GetBranch())) for c in changes))
Sigurd Schneider1bfda8e2021-06-30 14:46:25 +00004310
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004311 if options.date_order or settings.IsStatusCommitOrderByDate():
4312 sorted_changes = sorted(changes,
4313 key=lambda c: c.GetCommitDate(),
4314 reverse=True)
4315 else:
4316 sorted_changes = sorted(changes, key=lambda c: c.GetBranch())
4317 for cl in sorted_changes:
4318 branch = cl.GetBranch()
4319 while branch not in branch_statuses:
4320 c, status = next(output)
4321 branch_statuses[c.GetBranch()] = status
4322 status = branch_statuses.pop(branch)
4323 url = cl.GetIssueURL(short=True)
4324 if url and (not status or status == 'error'):
4325 # The issue probably doesn't exist anymore.
4326 url += ' (broken)'
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004327
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004328 color = color_for_status(status)
4329 # Turn off bold as well as colors.
4330 END = '\033[0m'
4331 reset = Fore.RESET + END
4332 if not setup_color.IS_TTY:
4333 color = ''
4334 reset = ''
4335 status_str = '(%s)' % status if status else ''
Daniel McArdle452a49f2019-02-14 17:28:31 +00004336
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004337 branch_display = FormatBranchName(branch)
4338 padding = ' ' * (alignment - len(branch_display))
4339 if not options.no_branch_color:
4340 branch_display = FormatBranchName(branch, colorize=True)
Daniel McArdle452a49f2019-02-14 17:28:31 +00004341
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004342 print(' %s : %s%s %s%s' %
4343 (padding + branch_display, color, url, status_str, reset))
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004344
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004345 print()
4346 print('Current branch: %s' % current_branch)
4347 for cl in changes:
4348 if cl.GetBranch() == current_branch:
4349 break
4350 if not cl.GetIssue():
4351 print('No issue assigned.')
4352 return 0
4353 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4354 if not options.fast:
4355 print('Issue description:')
4356 print(cl.FetchDescription(pretty=True))
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004357 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004358
4359
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004360def colorize_CMDstatus_doc():
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004361 """To be called once in main() to add colors to git cl status help."""
4362 colors = [i for i in dir(Fore) if i[0].isupper()]
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004363
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004364 def colorize_line(line):
4365 for color in colors:
4366 if color in line.upper():
4367 # Extract whitespace first and the leading '-'.
4368 indent = len(line) - len(line.lstrip(' ')) + 1
4369 return line[:indent] + getattr(
4370 Fore, color) + line[indent:] + Fore.RESET
4371 return line
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004372
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004373 lines = CMDstatus.__doc__.splitlines()
4374 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004375
4376
phajdan.jre328cf92016-08-22 04:12:17 -07004377def write_json(path, contents):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004378 if path == '-':
4379 json.dump(contents, sys.stdout)
4380 else:
4381 with open(path, 'w') as f:
4382 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004383
4384
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004385@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004386@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004387def CMDissue(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004388 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004389
4390 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004391 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004392 parser.add_option('-r',
4393 '--reverse',
4394 action='store_true',
4395 help='Lookup the branch(es) for the specified issues. If '
4396 'no issues are specified, all branches with mapped '
4397 'issues will be listed.')
4398 parser.add_option('--json',
4399 help='Path to JSON output file, or "-" for stdout.')
4400 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004401
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004402 if options.reverse:
4403 branches = RunGit(['for-each-ref', 'refs/heads',
4404 '--format=%(refname)']).splitlines()
4405 # Reverse issue lookup.
4406 issue_branch_map = {}
Arthur Milchior801a9752023-04-07 10:33:54 +00004407
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004408 git_config = {}
4409 for config in RunGit(['config', '--get-regexp',
4410 r'branch\..*issue']).splitlines():
4411 name, _space, val = config.partition(' ')
4412 git_config[name] = val
Arthur Milchior801a9752023-04-07 10:33:54 +00004413
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004414 for branch in branches:
4415 issue = git_config.get(
4416 'branch.%s.%s' %
4417 (scm.GIT.ShortBranchName(branch), ISSUE_CONFIG_KEY))
4418 if issue:
4419 issue_branch_map.setdefault(int(issue), []).append(branch)
4420 if not args:
4421 args = sorted(issue_branch_map.keys())
4422 result = {}
4423 for issue in args:
4424 try:
4425 issue_num = int(issue)
4426 except ValueError:
4427 print('ERROR cannot parse issue number: %s' % issue,
4428 file=sys.stderr)
4429 continue
4430 result[issue_num] = issue_branch_map.get(issue_num)
4431 print('Branch for issue number %s: %s' % (issue, ', '.join(
4432 issue_branch_map.get(issue_num) or ('None', ))))
4433 if options.json:
4434 write_json(options.json, result)
4435 return 0
4436
4437 if len(args) > 0:
4438 issue = ParseIssueNumberArgument(args[0])
4439 if not issue.valid:
4440 DieWithError(
4441 'Pass a url or number to set the issue, 0 to unset it, '
4442 'or no argument to list it.\n'
4443 'Maybe you want to run git cl status?')
4444 cl = Changelist()
4445 cl.SetIssue(issue.issue)
4446 else:
4447 cl = Changelist()
4448 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
Arthur Milchior801a9752023-04-07 10:33:54 +00004449 if options.json:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004450 write_json(
4451 options.json, {
4452 'gerrit_host': cl.GetGerritHost(),
4453 'gerrit_project': cl.GetGerritProject(),
4454 'issue_url': cl.GetIssueURL(),
4455 'issue': cl.GetIssue(),
4456 })
Arthur Milchior801a9752023-04-07 10:33:54 +00004457 return 0
Aaron Gable78753da2017-06-15 10:35:49 -07004458
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004459
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004460@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004461def CMDcomments(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004462 """Shows or posts review comments for any changelist."""
4463 parser.add_option('-a',
4464 '--add-comment',
4465 dest='comment',
4466 help='comment to add to an issue')
4467 parser.add_option('-p',
4468 '--publish',
4469 action='store_true',
4470 help='marks CL as ready and sends comment to reviewers')
4471 parser.add_option('-i',
4472 '--issue',
4473 dest='issue',
4474 help='review issue id (defaults to current issue).')
4475 parser.add_option('-m',
4476 '--machine-readable',
4477 dest='readable',
4478 action='store_false',
4479 default=True,
4480 help='output comments in a format compatible with '
4481 'editor parsing')
4482 parser.add_option('-j',
4483 '--json-file',
4484 help='File to write JSON summary to, or "-" for stdout')
4485 options, args = parser.parse_args(args)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004486
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004487 issue = None
4488 if options.issue:
4489 try:
4490 issue = int(options.issue)
4491 except ValueError:
4492 DieWithError('A review issue ID is expected to be a number.')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004493
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004494 cl = Changelist(issue=issue)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004495
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004496 if options.comment:
4497 cl.AddComment(options.comment, options.publish)
4498 return 0
4499
4500 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4501 key=lambda c: c.date)
4502 for comment in summary:
4503 if comment.disapproval:
4504 color = Fore.RED
4505 elif comment.approval:
4506 color = Fore.GREEN
4507 elif comment.sender == cl.GetIssueOwner():
4508 color = Fore.MAGENTA
4509 elif comment.autogenerated:
4510 color = Fore.CYAN
4511 else:
4512 color = Fore.BLUE
4513 print('\n%s%s %s%s\n%s' %
4514 (color, comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4515 comment.sender, Fore.RESET, '\n'.join(
4516 ' ' + l for l in comment.message.strip().splitlines())))
4517
4518 if options.json_file:
4519
4520 def pre_serialize(c):
4521 dct = c._asdict().copy()
4522 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4523 return dct
4524
4525 write_json(options.json_file, [pre_serialize(x) for x in summary])
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004526 return 0
4527
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004528
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004529@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004530@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004531def CMDdescription(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004532 """Brings up the editor for the current CL's description."""
4533 parser.add_option(
4534 '-d',
4535 '--display',
4536 action='store_true',
4537 help='Display the description instead of opening an editor')
4538 parser.add_option(
4539 '-n',
4540 '--new-description',
4541 help='New description to set for this issue (- for stdin, '
4542 '+ to load from local commit HEAD)')
4543 parser.add_option('-f',
4544 '--force',
4545 action='store_true',
4546 help='Delete any unpublished Gerrit edits for this issue '
4547 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004548
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004549 options, args = parser.parse_args(args)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004550
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004551 target_issue_arg = None
4552 if len(args) > 0:
4553 target_issue_arg = ParseIssueNumberArgument(args[0])
4554 if not target_issue_arg.valid:
4555 parser.error('Invalid issue ID or URL.')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004556
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004557 kwargs = {}
4558 if target_issue_arg:
4559 kwargs['issue'] = target_issue_arg.issue
4560 kwargs['codereview_host'] = target_issue_arg.hostname
martiniss6eda05f2016-06-30 10:18:35 -07004561
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004562 cl = Changelist(**kwargs)
4563 if not cl.GetIssue():
4564 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004565
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004566 if args and not args[0].isdigit():
4567 logging.info('canonical issue/change URL: %s\n', cl.GetIssueURL())
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004568
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004569 description = ChangeDescription(cl.FetchDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004570
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004571 if options.display:
4572 print(description.description)
4573 return 0
4574
4575 if options.new_description:
4576 text = options.new_description
4577 if text == '-':
4578 text = '\n'.join(l.rstrip() for l in sys.stdin)
4579 elif text == '+':
4580 base_branch = cl.GetCommonAncestorWithUpstream()
4581 text = _create_description_from_log([base_branch])
4582
4583 description.set_description(text)
4584 else:
4585 description.prompt()
4586 if cl.FetchDescription().strip() != description.description:
4587 cl.UpdateDescription(description.description, force=options.force)
smut@google.com34fb6b12015-07-13 20:03:26 +00004588 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004589
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004590
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004591@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004592def CMDlint(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004593 """Runs cpplint on the current changelist."""
4594 parser.add_option(
4595 '--filter',
4596 action='append',
4597 metavar='-x,+y',
4598 help='Comma-separated list of cpplint\'s category-filters')
4599 options, args = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004600
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004601 # Access to a protected member _XX of a client class
4602 # pylint: disable=protected-access
4603 try:
4604 import cpplint
4605 import cpplint_chromium
4606 except ImportError:
4607 print(
4608 'Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.'
4609 )
4610 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004611
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004612 # Change the current working directory before calling lint so that it
4613 # shows the correct base.
4614 previous_cwd = os.getcwd()
4615 os.chdir(settings.GetRoot())
4616 try:
4617 cl = Changelist()
4618 files = cl.GetAffectedFiles(cl.GetCommonAncestorWithUpstream())
4619 if not files:
4620 print('Cannot lint an empty CL')
4621 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004622
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004623 # Process cpplint arguments, if any.
4624 filters = presubmit_canned_checks.GetCppLintFilters(options.filter)
4625 command = ['--filter=' + ','.join(filters)]
4626 command.extend(args)
4627 command.extend(files)
4628 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004629
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004630 include_regex = re.compile(settings.GetLintRegex())
4631 ignore_regex = re.compile(settings.GetLintIgnoreRegex())
4632 extra_check_functions = [
4633 cpplint_chromium.CheckPointerDeclarationWhitespace
4634 ]
4635 for filename in filenames:
4636 if not include_regex.match(filename):
4637 print('Skipping file %s' % filename)
4638 continue
Lei Zhang379d1ad2020-07-15 19:40:06 +00004639
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004640 if ignore_regex.match(filename):
4641 print('Ignoring file %s' % filename)
4642 continue
Lei Zhang379d1ad2020-07-15 19:40:06 +00004643
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004644 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4645 extra_check_functions)
4646 finally:
4647 os.chdir(previous_cwd)
4648 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
4649 if cpplint._cpplint_state.error_count != 0:
4650 return 1
4651 return 0
thestig@chromium.org44202a22014-03-11 19:22:18 +00004652
4653
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004654@metrics.collector.collect_metrics('git cl presubmit')
mlcuic601e362023-08-14 23:39:46 +00004655@subcommand.usage('[base branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004656def CMDpresubmit(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004657 """Runs presubmit tests on the current changelist."""
4658 parser.add_option('-u',
4659 '--upload',
4660 action='store_true',
4661 help='Run upload hook instead of the push hook')
4662 parser.add_option('-f',
4663 '--force',
4664 action='store_true',
4665 help='Run checks even if tree is dirty')
4666 parser.add_option(
4667 '--all',
4668 action='store_true',
4669 help='Run checks against all files, not just modified ones')
4670 parser.add_option('--files',
4671 nargs=1,
4672 help='Semicolon-separated list of files to be marked as '
4673 'modified when executing presubmit or post-upload hooks. '
4674 'fnmatch wildcards can also be used.')
4675 parser.add_option(
4676 '--parallel',
4677 action='store_true',
4678 help='Run all tests specified by input_api.RunTests in all '
4679 'PRESUBMIT files in parallel.')
4680 parser.add_option('--resultdb',
4681 action='store_true',
4682 help='Run presubmit checks in the ResultSink environment '
4683 'and send results to the ResultDB database.')
4684 parser.add_option('--realm', help='LUCI realm if reporting to ResultDB')
4685 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004686
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004687 if not options.force and git_common.is_dirty_git_tree('presubmit'):
4688 print('use --force to check even if tree is dirty.')
4689 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004690
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004691 cl = Changelist()
4692 if args:
4693 base_branch = args[0]
Bruce Dawsoneb8426e2022-08-05 23:58:15 +00004694 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004695 # Default to diffing against the common ancestor of the upstream branch.
4696 base_branch = cl.GetCommonAncestorWithUpstream()
Aaron Gable8076c282017-11-29 14:39:41 -08004697
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004698 start = time.time()
4699 try:
4700 if not 'PRESUBMIT_SKIP_NETWORK' in os.environ and cl.GetIssue():
4701 description = cl.FetchDescription()
4702 else:
4703 description = _create_description_from_log([base_branch])
4704 except Exception as e:
4705 print('Failed to fetch CL description - %s' % str(e))
4706 description = _create_description_from_log([base_branch])
4707 elapsed = time.time() - start
4708 if elapsed > 5:
4709 print('%.1f s to get CL description.' % elapsed)
Bruce Dawson13acea32022-05-03 22:13:08 +00004710
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004711 if not base_branch:
4712 if not options.force:
4713 print('use --force to check even when not on a branch.')
4714 return 1
4715 base_branch = 'HEAD'
4716
4717 cl.RunHook(committing=not options.upload,
4718 may_prompt=False,
4719 verbose=options.verbose,
4720 parallel=options.parallel,
4721 upstream=base_branch,
4722 description=description,
4723 all_files=options.all,
4724 files=options.files,
4725 resultdb=options.resultdb,
4726 realm=options.realm)
4727 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004728
4729
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004730def GenerateGerritChangeId(message):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004731 """Returns the Change ID footer value (Ixxxxxx...xxx).
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004732
4733 Works the same way as
4734 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4735 but can be called on demand on all platforms.
4736
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004737 The basic idea is to generate git hash of a state of the tree, original
4738 commit message, author/committer info and timestamps.
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004739 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004740 lines = []
4741 tree_hash = RunGitSilent(['write-tree'])
4742 lines.append('tree %s' % tree_hash.strip())
4743 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'],
4744 suppress_stderr=False)
4745 if code == 0:
4746 lines.append('parent %s' % parent.strip())
4747 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4748 lines.append('author %s' % author.strip())
4749 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4750 lines.append('committer %s' % committer.strip())
4751 lines.append('')
4752 # Note: Gerrit's commit-hook actually cleans message of some lines and
4753 # whitespace. This code is not doing this, but it clearly won't decrease
4754 # entropy.
4755 lines.append(message)
4756 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4757 stdin=('\n'.join(lines)).encode())
4758 return 'I%s' % change_hash.strip()
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004759
4760
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004761def GetTargetRef(remote, remote_branch, target_branch):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004762 """Computes the remote branch ref to use for the CL.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004763
4764 Args:
4765 remote (str): The git remote for the CL.
4766 remote_branch (str): The git remote branch for the CL.
4767 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004768 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004769 if not (remote and remote_branch):
4770 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004771
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004772 if target_branch:
4773 # Canonicalize branch references to the equivalent local full symbolic
4774 # refs, which are then translated into the remote full symbolic refs
4775 # below.
4776 if '/' not in target_branch:
4777 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4778 else:
4779 prefix_replacements = (
4780 ('^((refs/)?remotes/)?branch-heads/',
4781 'refs/remotes/branch-heads/'),
4782 ('^((refs/)?remotes/)?%s/' % remote,
4783 'refs/remotes/%s/' % remote),
4784 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4785 )
4786 match = None
4787 for regex, replacement in prefix_replacements:
4788 match = re.search(regex, target_branch)
4789 if match:
4790 remote_branch = target_branch.replace(
4791 match.group(0), replacement)
4792 break
4793 if not match:
4794 # This is a branch path but not one we recognize; use as-is.
4795 remote_branch = target_branch
4796 # pylint: disable=consider-using-get
4797 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4798 # pylint: enable=consider-using-get
4799 # Handle the refs that need to land in different refs.
4800 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004801
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004802 # Create the true path to the remote branch.
4803 # Does the following translation:
4804 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4805 # * refs/remotes/origin/main -> refs/heads/main
4806 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4807 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4808 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4809 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4810 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4811 'refs/heads/')
4812 elif remote_branch.startswith('refs/remotes/branch-heads'):
4813 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004814
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004815 return remote_branch
wittman@chromium.org455dc922015-01-26 20:15:50 +00004816
4817
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004818def cleanup_list(l):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004819 """Fixes a list so that comma separated items are put as individual items.
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004820
4821 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4822 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4823 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004824 items = sum((i.split(',') for i in l), [])
4825 stripped_items = (i.strip() for i in items)
4826 return sorted(filter(None, stripped_items))
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004827
4828
Aaron Gable4db38df2017-11-03 14:59:07 -07004829@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004830@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004831def CMDupload(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004832 """Uploads the current changelist to codereview.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004833
4834 Can skip dependency patchset uploads for a branch by running:
4835 git config branch.branch_name.skip-deps-uploads True
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004836 To unset, run:
rmistry@google.com78948ed2015-07-08 23:09:57 +00004837 git config --unset branch.branch_name.skip-deps-uploads
4838 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004839
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004840 If the name of the checked out branch starts with "bug-" or "fix-" followed
4841 by a bug number, this bug number is automatically populated in the CL
Dominic Battre7d1c4842017-10-27 09:17:28 +02004842 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004843
4844 If subject contains text in square brackets or has "<text>: " prefix, such
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004845 text(s) is treated as Gerrit hashtags. For example, CLs with subjects:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004846 [git-cl] add support for hashtags
4847 Foo bar: implement foo
4848 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004849 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004850 parser.add_option('--bypass-hooks',
4851 action='store_true',
4852 dest='bypass_hooks',
4853 help='bypass upload presubmit hook')
4854 parser.add_option('--bypass-watchlists',
4855 action='store_true',
4856 dest='bypass_watchlists',
4857 help='bypass watchlists auto CC-ing reviewers')
4858 parser.add_option('-f',
4859 '--force',
4860 action='store_true',
4861 dest='force',
4862 help="force yes to questions (don't prompt)")
4863 parser.add_option('--message',
4864 '-m',
4865 dest='message',
4866 help='message for patchset')
4867 parser.add_option('-b',
4868 '--bug',
4869 help='pre-populate the bug number(s) for this issue. '
4870 'If several, separate with commas')
4871 parser.add_option('--message-file',
4872 dest='message_file',
4873 help='file which contains message for patchset')
4874 parser.add_option('--title', '-t', dest='title', help='title for patchset')
4875 parser.add_option('-T',
4876 '--skip-title',
4877 action='store_true',
4878 dest='skip_title',
4879 help='Use the most recent commit message as the title of '
4880 'the patchset')
4881 parser.add_option('-r',
4882 '--reviewers',
4883 action='append',
4884 default=[],
4885 help='reviewer email addresses')
4886 parser.add_option('--cc',
4887 action='append',
4888 default=[],
4889 help='cc email addresses')
4890 parser.add_option('--hashtag',
4891 dest='hashtags',
4892 action='append',
4893 default=[],
4894 help=('Gerrit hashtag for new CL; '
4895 'can be applied multiple times'))
4896 parser.add_option('-s',
4897 '--send-mail',
4898 '--send-email',
4899 dest='send_mail',
4900 action='store_true',
4901 help='send email to reviewer(s) and cc(s) immediately')
4902 parser.add_option('--target_branch',
4903 '--target-branch',
4904 metavar='TARGET',
4905 help='Apply CL to remote ref TARGET. ' +
4906 'Default: remote branch head, or main')
4907 parser.add_option('--squash',
4908 action='store_true',
4909 help='Squash multiple commits into one')
4910 parser.add_option('--no-squash',
4911 action='store_false',
4912 dest='squash',
4913 help='Don\'t squash multiple commits into one')
4914 parser.add_option('--topic',
4915 default=None,
4916 help='Topic to specify when uploading')
4917 parser.add_option('--r-owners',
4918 dest='add_owners_to',
4919 action='store_const',
4920 const='R',
4921 help='add a set of OWNERS to R')
4922 parser.add_option('-c',
4923 '--use-commit-queue',
4924 action='store_true',
4925 default=False,
4926 help='tell the CQ to commit this patchset; '
4927 'implies --send-mail')
4928 parser.add_option('-d',
4929 '--cq-dry-run',
4930 action='store_true',
4931 default=False,
4932 help='Send the patchset to do a CQ dry run right after '
4933 'upload.')
4934 parser.add_option('--set-bot-commit',
4935 action='store_true',
4936 help=optparse.SUPPRESS_HELP)
4937 parser.add_option('--preserve-tryjobs',
4938 action='store_true',
4939 help='instruct the CQ to let tryjobs running even after '
4940 'new patchsets are uploaded instead of canceling '
4941 'prior patchset\' tryjobs')
4942 parser.add_option(
4943 '--dependencies',
4944 action='store_true',
4945 help='Uploads CLs of all the local branches that depend on '
4946 'the current branch')
4947 parser.add_option(
4948 '-a',
4949 '--enable-auto-submit',
4950 action='store_true',
4951 help='Sends your change to the CQ after an approval. Only '
4952 'works on repos that have the Auto-Submit label '
4953 'enabled')
4954 parser.add_option(
4955 '--parallel',
4956 action='store_true',
4957 help='Run all tests specified by input_api.RunTests in all '
4958 'PRESUBMIT files in parallel.')
4959 parser.add_option('--no-autocc',
4960 action='store_true',
4961 help='Disables automatic addition of CC emails')
4962 parser.add_option('--private',
4963 action='store_true',
4964 help='Set the review private. This implies --no-autocc.')
4965 parser.add_option('-R',
4966 '--retry-failed',
4967 action='store_true',
4968 help='Retry failed tryjobs from old patchset immediately '
4969 'after uploading new patchset. Cannot be used with '
4970 '--use-commit-queue or --cq-dry-run.')
4971 parser.add_option('--fixed',
4972 '-x',
4973 help='List of bugs that will be commented on and marked '
4974 'fixed (pre-populates "Fixed:" tag). Same format as '
4975 '-b option / "Bug:" tag. If fixing several issues, '
4976 'separate with commas.')
4977 parser.add_option('--edit-description',
4978 action='store_true',
4979 default=False,
4980 help='Modify description before upload. Cannot be used '
4981 'with --force. It is a noop when --no-squash is set '
4982 'or a new commit is created.')
4983 parser.add_option('--git-completion-helper',
4984 action="store_true",
4985 help=optparse.SUPPRESS_HELP)
4986 parser.add_option('-o',
4987 '--push-options',
4988 action='append',
4989 default=[],
4990 help='Transmit the given string to the server when '
4991 'performing git push (pass-through). See git-push '
4992 'documentation for more details.')
4993 parser.add_option('--no-add-changeid',
4994 action='store_true',
4995 dest='no_add_changeid',
4996 help='Do not add change-ids to messages.')
4997 parser.add_option('--cherry-pick-stacked',
4998 '--cp',
4999 dest='cherry_pick_stacked',
5000 action='store_true',
5001 help='If parent branch has un-uploaded updates, '
5002 'automatically skip parent branches and just upload '
5003 'the current branch cherry-pick on its parent\'s last '
5004 'uploaded commit. Allows users to skip the potential '
5005 'interactive confirmation step.')
5006 # TODO(b/265929888): Add --wip option of --cl-status option.
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00005007
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005008 orig_args = args
5009 (options, args) = parser.parse_args(args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005010
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005011 if options.git_completion_helper:
5012 print(' '.join(opt.get_opt_string() for opt in parser.option_list
5013 if opt.help != optparse.SUPPRESS_HELP))
5014 return
Ng Zhi Ancdaf0be2020-05-27 20:57:28 +00005015
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005016 # TODO(crbug.com/1475405): Warn users if the project uses submodules and
5017 # they have fsmonitor enabled.
5018 if os.path.isfile('.gitmodules'):
5019 git_common.warn_submodule()
Aravind Vasudevanb8164182023-08-25 21:49:12 +00005020
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005021 if git_common.is_dirty_git_tree('upload'):
5022 return 1
ukai@chromium.orge8077812012-02-03 03:41:46 +00005023
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005024 options.reviewers = cleanup_list(options.reviewers)
5025 options.cc = cleanup_list(options.cc)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005026
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005027 if options.edit_description and options.force:
5028 parser.error('Only one of --force and --edit-description allowed')
Josipe827b0f2020-01-30 00:07:20 +00005029
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005030 if options.message_file:
5031 if options.message:
5032 parser.error('Only one of --message and --message-file allowed.')
5033 options.message = gclient_utils.FileRead(options.message_file)
tandriib80458a2016-06-23 12:20:07 -07005034
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005035 if ([options.cq_dry_run, options.use_commit_queue, options.retry_failed
5036 ].count(True) > 1):
5037 parser.error('Only one of --use-commit-queue, --cq-dry-run or '
5038 '--retry-failed is allowed.')
tandrii4d0545a2016-07-06 03:56:49 -07005039
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005040 if options.skip_title and options.title:
5041 parser.error('Only one of --title and --skip-title allowed.')
Mario Bianuccicebfb4e2020-07-22 23:08:16 +00005042
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005043 if options.use_commit_queue:
5044 options.send_mail = True
Aaron Gableedbc4132017-09-11 13:22:28 -07005045
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005046 if options.squash is None:
5047 # Load default for user, repo, squash=true, in this order.
5048 options.squash = settings.GetSquashGerritUploads()
Edward Lesmes0dd54822020-03-26 18:24:25 +00005049
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005050 cl = Changelist(branchref=options.target_branch)
Joanna Wang5051ffe2023-03-01 22:24:07 +00005051
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005052 # Warm change details cache now to avoid RPCs later, reducing latency for
5053 # developers.
5054 if cl.GetIssue():
5055 cl._GetChangeDetail([
5056 'DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'
5057 ])
Joanna Wang5051ffe2023-03-01 22:24:07 +00005058
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005059 if options.retry_failed and not cl.GetIssue():
5060 print('No previous patchsets, so --retry-failed has no effect.')
5061 options.retry_failed = False
Joanna Wang5051ffe2023-03-01 22:24:07 +00005062
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005063 disable_dogfood_stacked_changes = os.environ.get(
5064 DOGFOOD_STACKED_CHANGES_VAR) == '0'
5065 dogfood_stacked_changes = os.environ.get(DOGFOOD_STACKED_CHANGES_VAR) == '1'
Joanna Wang5051ffe2023-03-01 22:24:07 +00005066
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005067 # Only print message for folks who don't have DOGFOOD_STACKED_CHANGES set
5068 # to an expected value.
5069 if (options.squash and not dogfood_stacked_changes
5070 and not disable_dogfood_stacked_changes):
5071 print(
5072 'This repo has been enrolled in the stacked changes dogfood.\n'
5073 '`git cl upload` now uploads the current branch and all upstream '
5074 'branches that have un-uploaded updates.\n'
5075 'Patches can now be reapplied with --force:\n'
5076 '`git cl patch --reapply --force`.\n'
5077 'Googlers may visit go/stacked-changes-dogfood for more information.\n'
5078 '\n'
5079 'Depot Tools no longer sets new uploads to "WIP". Please update the\n'
5080 '"Set new changes to "work in progress" by default" checkbox at\n'
5081 'https://<host>-review.googlesource.com/settings/\n'
5082 '\n'
5083 'To opt-out use `export DOGFOOD_STACKED_CHANGES=0`.\n'
5084 'To hide this message use `export DOGFOOD_STACKED_CHANGES=1`.\n'
5085 'File bugs at https://bit.ly/3Y6opoI\n')
Joanna Wang4786a412023-05-16 18:23:08 +00005086
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005087 if options.squash and not disable_dogfood_stacked_changes:
5088 if options.dependencies:
5089 parser.error(
5090 '--dependencies is not available for this dogfood workflow.')
Joanna Wang5051ffe2023-03-01 22:24:07 +00005091
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005092 if options.cherry_pick_stacked:
5093 try:
5094 orig_args.remove('--cherry-pick-stacked')
5095 except ValueError:
5096 orig_args.remove('--cp')
5097 UploadAllSquashed(options, orig_args)
5098 return 0
Joanna Wang18de1f62023-01-21 01:24:24 +00005099
Joanna Wangd75fc882023-03-01 21:53:34 +00005100 if options.cherry_pick_stacked:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005101 parser.error(
5102 '--cherry-pick-stacked is not available for this workflow.')
Joanna Wang18de1f62023-01-21 01:24:24 +00005103
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005104 # cl.GetMostRecentPatchset uses cached information, and can return the last
5105 # patchset before upload. Calling it here makes it clear that it's the
5106 # last patchset before upload. Note that GetMostRecentPatchset will fail
5107 # if no CL has been uploaded yet.
5108 if options.retry_failed:
5109 patchset = cl.GetMostRecentPatchset()
Joanna Wangd75fc882023-03-01 21:53:34 +00005110
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005111 ret = cl.CMDUpload(options, args, orig_args)
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00005112
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005113 if options.retry_failed:
5114 if ret != 0:
5115 print('Upload failed, so --retry-failed has no effect.')
5116 return ret
5117 builds, _ = _fetch_latest_builds(cl,
5118 DEFAULT_BUILDBUCKET_HOST,
5119 latest_patchset=patchset)
5120 jobs = _filter_failed_for_retry(builds)
5121 if len(jobs) == 0:
5122 print('No failed tryjobs, so --retry-failed has no effect.')
5123 return ret
5124 _trigger_tryjobs(cl, jobs, options, patchset + 1)
Quinten Yearsleya19d3532019-09-30 21:54:39 +00005125
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005126 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00005127
5128
Daniel Cheng66d0f152023-08-29 23:21:58 +00005129def UploadAllSquashed(options: optparse.Values,
5130 orig_args: Sequence[str]) -> int:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005131 """Uploads the current and upstream branches (if necessary)."""
5132 cls, cherry_pick_current = _UploadAllPrecheck(options, orig_args)
Joanna Wang18de1f62023-01-21 01:24:24 +00005133
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005134 # Create commits.
5135 uploads_by_cl: List[Tuple[Changelist, _NewUpload]] = []
5136 if cherry_pick_current:
5137 parent = cls[1]._GitGetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY)
5138 new_upload = cls[0].PrepareCherryPickSquashedCommit(options, parent)
5139 uploads_by_cl.append((cls[0], new_upload))
5140 else:
5141 ordered_cls = list(reversed(cls))
Joanna Wangc710e2d2023-01-25 14:53:22 +00005142
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005143 cl = ordered_cls[0]
5144 # We can only support external changes when we're only uploading one
5145 # branch.
5146 parent = cl._UpdateWithExternalChanges() if len(
5147 ordered_cls) == 1 else None
5148 orig_parent = None
5149 if parent is None:
5150 origin = '.'
5151 branch = cl.GetBranch()
Joanna Wang74c53b62023-03-01 22:00:22 +00005152
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005153 while origin == '.':
5154 # Search for cl's closest ancestor with a gerrit hash.
5155 origin, upstream_branch_ref = Changelist.FetchUpstreamTuple(
5156 branch)
5157 if origin == '.':
5158 upstream_branch = scm.GIT.ShortBranchName(
5159 upstream_branch_ref)
Joanna Wang7603f042023-03-01 22:17:36 +00005160
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005161 # Support the `git merge` and `git pull` workflow.
5162 if upstream_branch in ['master', 'main']:
5163 parent = cl.GetCommonAncestorWithUpstream()
5164 else:
5165 orig_parent = scm.GIT.GetBranchConfig(
5166 settings.GetRoot(), upstream_branch,
5167 LAST_UPLOAD_HASH_CONFIG_KEY)
5168 parent = scm.GIT.GetBranchConfig(
5169 settings.GetRoot(), upstream_branch,
5170 GERRIT_SQUASH_HASH_CONFIG_KEY)
5171 if parent:
5172 break
5173 branch = upstream_branch
5174 else:
5175 # Either the root of the tree is the cl's direct parent and the
5176 # while loop above only found empty branches between cl and the
5177 # root of the tree.
5178 parent = cl.GetCommonAncestorWithUpstream()
Joanna Wang6215dd02023-02-07 15:58:03 +00005179
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005180 if orig_parent is None:
5181 orig_parent = parent
5182 for i, cl in enumerate(ordered_cls):
5183 # If we're in the middle of the stack, set end_commit to
5184 # downstream's direct ancestor.
5185 if i + 1 < len(ordered_cls):
5186 child_base_commit = ordered_cls[
5187 i + 1].GetCommonAncestorWithUpstream()
5188 else:
5189 child_base_commit = None
5190 new_upload = cl.PrepareSquashedCommit(options,
5191 parent,
5192 orig_parent,
5193 end_commit=child_base_commit)
5194 uploads_by_cl.append((cl, new_upload))
5195 parent = new_upload.commit_to_push
5196 orig_parent = child_base_commit
Joanna Wangc710e2d2023-01-25 14:53:22 +00005197
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005198 # Create refspec options
5199 cl, new_upload = uploads_by_cl[-1]
5200 refspec_opts = cl._GetRefSpecOptions(
5201 options,
5202 new_upload.change_desc,
5203 multi_change_upload=len(uploads_by_cl) > 1,
5204 dogfood_path=True)
5205 refspec_suffix = ''
5206 if refspec_opts:
5207 refspec_suffix = '%' + ','.join(refspec_opts)
5208 assert ' ' not in refspec_suffix, (
5209 'spaces not allowed in refspec: "%s"' % refspec_suffix)
Joanna Wangc710e2d2023-01-25 14:53:22 +00005210
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005211 remote, remote_branch = cl.GetRemoteBranch()
5212 branch = GetTargetRef(remote, remote_branch, options.target_branch)
5213 refspec = '%s:refs/for/%s%s' % (new_upload.commit_to_push, branch,
5214 refspec_suffix)
Joanna Wangc710e2d2023-01-25 14:53:22 +00005215
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005216 # Git push
5217 git_push_metadata = {
5218 'gerrit_host':
5219 cl.GetGerritHost(),
5220 'title':
5221 options.title or '<untitled>',
5222 'change_id':
5223 git_footers.get_footer_change_id(new_upload.change_desc.description),
5224 'description':
5225 new_upload.change_desc.description,
5226 }
5227 push_stdout = cl._RunGitPushWithTraces(refspec, refspec_opts,
5228 git_push_metadata,
5229 options.push_options)
Joanna Wangc710e2d2023-01-25 14:53:22 +00005230
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005231 # Post push updates
5232 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
5233 change_numbers = [
5234 m.group(1) for m in map(regex.match, push_stdout.splitlines()) if m
5235 ]
Joanna Wangc710e2d2023-01-25 14:53:22 +00005236
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005237 for i, (cl, new_upload) in enumerate(uploads_by_cl):
5238 cl.PostUploadUpdates(options, new_upload, change_numbers[i])
Joanna Wangc710e2d2023-01-25 14:53:22 +00005239
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005240 return 0
Joanna Wang18de1f62023-01-21 01:24:24 +00005241
5242
5243def _UploadAllPrecheck(options, orig_args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005244 # type: (optparse.Values, Sequence[str]) -> Tuple[Sequence[Changelist],
5245 # bool]
5246 """Checks the state of the tree and gives the user uploading options
Joanna Wang18de1f62023-01-21 01:24:24 +00005247
5248 Returns: A tuple of the ordered list of changes that have new commits
5249 since their last upload and a boolean of whether the user wants to
5250 cherry-pick and upload the current branch instead of uploading all cls.
5251 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005252 cl = Changelist()
5253 if cl.GetBranch() is None:
5254 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
Joanna Wang6b98cdc2023-02-16 00:37:20 +00005255
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005256 branch_ref = None
5257 cls = []
5258 must_upload_upstream = False
5259 first_pass = True
Joanna Wang18de1f62023-01-21 01:24:24 +00005260
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005261 Changelist._GerritCommitMsgHookCheck(offer_removal=not options.force)
Joanna Wang18de1f62023-01-21 01:24:24 +00005262
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005263 while True:
5264 if len(cls) > _MAX_STACKED_BRANCHES_UPLOAD:
5265 DieWithError(
5266 'More than %s branches in the stack have not been uploaded.\n'
5267 'Are your branches in a misconfigured state?\n'
5268 'If not, please upload some upstream changes first.' %
5269 (_MAX_STACKED_BRANCHES_UPLOAD))
Joanna Wang18de1f62023-01-21 01:24:24 +00005270
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005271 cl = Changelist(branchref=branch_ref)
Joanna Wang18de1f62023-01-21 01:24:24 +00005272
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005273 # Only add CL if it has anything to commit.
5274 base_commit = cl.GetCommonAncestorWithUpstream()
5275 end_commit = RunGit(['rev-parse', cl.GetBranchRef()]).strip()
Joanna Wang6215dd02023-02-07 15:58:03 +00005276
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005277 commit_summary = _GetCommitCountSummary(base_commit, end_commit)
5278 if commit_summary:
5279 cls.append(cl)
5280 if (not first_pass and
5281 cl._GitGetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY)
5282 is None):
5283 # We are mid-stack and the user must upload their upstream
5284 # branches.
5285 must_upload_upstream = True
5286 print(f'Found change with {commit_summary}...')
5287 elif first_pass: # The current branch has nothing to commit. Exit.
5288 DieWithError('Branch %s has nothing to commit' % cl.GetBranch())
5289 # Else: A mid-stack branch has nothing to commit. We do not add it to
5290 # cls.
5291 first_pass = False
Joanna Wang6215dd02023-02-07 15:58:03 +00005292
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005293 # Cases below determine if we should continue to traverse up the tree.
5294 origin, upstream_branch_ref = Changelist.FetchUpstreamTuple(
5295 cl.GetBranch())
5296 branch_ref = upstream_branch_ref # set branch for next run.
Joanna Wang18de1f62023-01-21 01:24:24 +00005297
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005298 upstream_branch = scm.GIT.ShortBranchName(upstream_branch_ref)
5299 upstream_last_upload = scm.GIT.GetBranchConfig(
5300 settings.GetRoot(), upstream_branch, LAST_UPLOAD_HASH_CONFIG_KEY)
Joanna Wang6215dd02023-02-07 15:58:03 +00005301
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005302 # Case 1: We've reached the beginning of the tree.
5303 if origin != '.':
5304 break
Joanna Wang18de1f62023-01-21 01:24:24 +00005305
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005306 # Case 2: If any upstream branches have never been uploaded,
5307 # the user MUST upload them unless they are empty. Continue to
5308 # next loop to add upstream if it is not empty.
5309 if not upstream_last_upload:
5310 continue
Joanna Wang18de1f62023-01-21 01:24:24 +00005311
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005312 # Case 3: If upstream's last_upload == cl.base_commit we do
5313 # not need to upload any more upstreams from this point on.
5314 # (Even if there may be diverged branches higher up the tree)
5315 if base_commit == upstream_last_upload:
5316 break
Joanna Wang18de1f62023-01-21 01:24:24 +00005317
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005318 # Case 4: If upstream's last_upload < cl.base_commit we are
5319 # uploading cl and upstream_cl.
5320 # Continue up the tree to check other branch relations.
5321 if scm.GIT.IsAncestor(upstream_last_upload, base_commit):
5322 continue
Joanna Wang18de1f62023-01-21 01:24:24 +00005323
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005324 # Case 5: If cl.base_commit < upstream's last_upload the user
5325 # must rebase before uploading.
5326 if scm.GIT.IsAncestor(base_commit, upstream_last_upload):
5327 DieWithError(
5328 'At least one branch in the stack has diverged from its upstream '
5329 'branch and does not contain its upstream\'s last upload.\n'
5330 'Please rebase the stack with `git rebase-update` before uploading.'
5331 )
Joanna Wang18de1f62023-01-21 01:24:24 +00005332
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005333 # The tree went through a rebase. LAST_UPLOAD_HASH_CONFIG_KEY no longer
5334 # has any relation to commits in the tree. Continue up the tree until we
5335 # hit the root.
Joanna Wang18de1f62023-01-21 01:24:24 +00005336
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005337 # We assume all cls in the stack have the same auth requirements and only
5338 # check this once.
5339 cls[0].EnsureAuthenticated(force=options.force)
Joanna Wang18de1f62023-01-21 01:24:24 +00005340
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005341 cherry_pick = False
5342 if len(cls) > 1:
5343 opt_message = ''
5344 branches = ', '.join([cl.branch for cl in cls])
5345 if len(orig_args):
5346 opt_message = ('options %s will be used for all uploads.\n' %
5347 orig_args)
5348 if must_upload_upstream:
5349 msg = ('At least one parent branch in `%s` has never been uploaded '
5350 'and must be uploaded before/with `%s`.\n' %
5351 (branches, cls[1].branch))
5352 if options.cherry_pick_stacked:
5353 DieWithError(msg)
5354 if not options.force:
5355 confirm_or_exit('\n' + opt_message + msg)
5356 else:
5357 if options.cherry_pick_stacked:
5358 print('cherry-picking `%s` on %s\'s last upload' %
5359 (cls[0].branch, cls[1].branch))
5360 cherry_pick = True
5361 elif not options.force:
5362 answer = gclient_utils.AskForData(
5363 '\n' + opt_message +
5364 'Press enter to update branches %s.\nOr type `n` to upload only '
5365 '`%s` cherry-picked on %s\'s last upload:' %
5366 (branches, cls[0].branch, cls[1].branch))
5367 if answer.lower() == 'n':
5368 cherry_pick = True
5369 return cls, cherry_pick
Joanna Wang18de1f62023-01-21 01:24:24 +00005370
5371
Francois Dorayd42c6812017-05-30 15:10:20 -04005372@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005373@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04005374def CMDsplit(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005375 """Splits a branch into smaller branches and uploads CLs.
Francois Dorayd42c6812017-05-30 15:10:20 -04005376
5377 Creates a branch and uploads a CL for each group of files modified in the
5378 current branch that share a common OWNERS file. In the CL description and
Edward Lemurac5c55f2020-02-29 00:17:16 +00005379 comment, the string '$directory', is replaced with the directory containing
5380 the shared OWNERS file.
Francois Dorayd42c6812017-05-30 15:10:20 -04005381 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005382 parser.add_option('-d',
5383 '--description',
5384 dest='description_file',
5385 help='A text file containing a CL description in which '
5386 '$directory will be replaced by each CL\'s directory.')
5387 parser.add_option('-c',
5388 '--comment',
5389 dest='comment_file',
5390 help='A text file containing a CL comment.')
5391 parser.add_option(
5392 '-n',
5393 '--dry-run',
5394 dest='dry_run',
5395 action='store_true',
5396 default=False,
5397 help='List the files and reviewers for each CL that would '
5398 'be created, but don\'t create branches or CLs.')
5399 parser.add_option('--cq-dry-run',
5400 action='store_true',
5401 help='If set, will do a cq dry run for each uploaded CL. '
5402 'Please be careful when doing this; more than ~10 CLs '
5403 'has the potential to overload our build '
5404 'infrastructure. Try to upload these not during high '
5405 'load times (usually 11-3 Mountain View time). Email '
5406 'infra-dev@chromium.org with any questions.')
5407 parser.add_option(
5408 '-a',
5409 '--enable-auto-submit',
5410 action='store_true',
5411 dest='enable_auto_submit',
5412 default=True,
5413 help='Sends your change to the CQ after an approval. Only '
5414 'works on repos that have the Auto-Submit label '
5415 'enabled')
5416 parser.add_option(
5417 '--disable-auto-submit',
5418 action='store_false',
5419 dest='enable_auto_submit',
5420 help='Disables automatic sending of the changes to the CQ '
5421 'after approval. Note that auto-submit only works for '
5422 'repos that have the Auto-Submit label enabled.')
5423 parser.add_option('--max-depth',
5424 type='int',
5425 default=0,
5426 help='The max depth to look for OWNERS files. Useful for '
5427 'controlling the granularity of the split CLs, e.g. '
5428 '--max-depth=1 will only split by top-level '
5429 'directory. Specifying a value less than 1 means no '
5430 'limit on max depth.')
5431 parser.add_option('--topic',
5432 default=None,
5433 help='Topic to specify when uploading')
5434 options, _ = parser.parse_args(args)
Francois Dorayd42c6812017-05-30 15:10:20 -04005435
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005436 if not options.description_file:
5437 parser.error('No --description flag specified.')
Francois Dorayd42c6812017-05-30 15:10:20 -04005438
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005439 def WrappedCMDupload(args):
5440 return CMDupload(OptionParser(), args)
Francois Dorayd42c6812017-05-30 15:10:20 -04005441
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005442 return split_cl.SplitCl(options.description_file, options.comment_file,
5443 Changelist, WrappedCMDupload, options.dry_run,
5444 options.cq_dry_run, options.enable_auto_submit,
5445 options.max_depth, options.topic,
5446 settings.GetRoot())
Francois Dorayd42c6812017-05-30 15:10:20 -04005447
5448
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005449@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005450@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005451def CMDdcommit(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005452 """DEPRECATED: Used to commit the current changelist via git-svn."""
5453 message = ('git-cl no longer supports committing to SVN repositories via '
5454 'git-svn. You probably want to use `git cl land` instead.')
5455 print(message)
5456 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005457
5458
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005459@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005460@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00005461def CMDland(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005462 """Commits the current changelist via git.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005463
5464 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
5465 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005466 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005467 parser.add_option('--bypass-hooks',
5468 action='store_true',
5469 dest='bypass_hooks',
5470 help='bypass upload presubmit hook')
5471 parser.add_option('-f',
5472 '--force',
5473 action='store_true',
5474 dest='force',
5475 help="force yes to questions (don't prompt)")
5476 parser.add_option(
5477 '--parallel',
5478 action='store_true',
5479 help='Run all tests specified by input_api.RunTests in all '
5480 'PRESUBMIT files in parallel.')
5481 parser.add_option('--resultdb',
5482 action='store_true',
5483 help='Run presubmit checks in the ResultSink environment '
5484 'and send results to the ResultDB database.')
5485 parser.add_option('--realm', help='LUCI realm if reporting to ResultDB')
5486 (options, args) = parser.parse_args(args)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005487
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005488 cl = Changelist()
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005489
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005490 if not cl.GetIssue():
5491 DieWithError('You must upload the change first to Gerrit.\n'
5492 ' If you would rather have `git cl land` upload '
5493 'automatically for you, see http://crbug.com/642759')
5494 return cl.CMDLand(options.force, options.bypass_hooks, options.verbose,
5495 options.parallel, options.resultdb, options.realm)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005496
5497
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005498@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005499@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005500def CMDpatch(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005501 """Applies (cherry-picks) a Gerrit changelist locally."""
5502 parser.add_option('-b',
5503 dest='newbranch',
5504 help='create a new branch off trunk for the patch')
5505 parser.add_option('-f',
5506 '--force',
5507 action='store_true',
5508 help='overwrite state on the current or chosen branch')
5509 parser.add_option('-n',
5510 '--no-commit',
5511 action='store_true',
5512 dest='nocommit',
5513 help='don\'t commit after patch applies.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005514
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005515 group = optparse.OptionGroup(
5516 parser,
5517 'Options for continuing work on the current issue uploaded from a '
5518 'different clone (e.g. different machine). Must be used independently '
5519 'from the other options. No issue number should be specified, and the '
5520 'branch must have an issue number associated with it')
5521 group.add_option('--reapply',
5522 action='store_true',
5523 dest='reapply',
5524 help='Reset the branch and reapply the issue.\n'
5525 'CAUTION: This will undo any local changes in this '
5526 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005527
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005528 group.add_option('--pull',
5529 action='store_true',
5530 dest='pull',
5531 help='Performs a pull before reapplying.')
5532 parser.add_option_group(group)
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005533
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005534 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005535
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005536 if options.reapply:
5537 if options.newbranch:
5538 parser.error('--reapply works on the current branch only.')
5539 if len(args) > 0:
5540 parser.error('--reapply implies no additional arguments.')
5541
5542 cl = Changelist()
5543 if not cl.GetIssue():
5544 parser.error('Current branch must have an associated issue.')
5545
5546 upstream = cl.GetUpstreamBranch()
5547 if upstream is None:
5548 parser.error('No upstream branch specified. Cannot reset branch.')
5549
5550 RunGit(['reset', '--hard', upstream])
5551 if options.pull:
5552 RunGit(['pull'])
5553
5554 target_issue_arg = ParseIssueNumberArgument(cl.GetIssue())
5555 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.nocommit,
5556 options.force, False)
5557
5558 if len(args) != 1 or not args[0]:
5559 parser.error('Must specify issue number or URL.')
5560
5561 target_issue_arg = ParseIssueNumberArgument(args[0])
5562 if not target_issue_arg.valid:
5563 parser.error('Invalid issue ID or URL.')
5564
5565 # We don't want uncommitted changes mixed up with the patch.
5566 if git_common.is_dirty_git_tree('patch'):
5567 return 1
5568
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005569 if options.newbranch:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005570 if options.force:
5571 RunGit(['branch', '-D', options.newbranch],
5572 stderr=subprocess2.PIPE,
5573 error_ok=True)
5574 git_new_branch.create_new_branch(options.newbranch)
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005575
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005576 cl = Changelist(codereview_host=target_issue_arg.hostname,
5577 issue=target_issue_arg.issue)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005578
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005579 if not args[0].isdigit():
5580 print('canonical issue/change URL: %s\n' % cl.GetIssueURL())
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005581
Joanna Wang44e9bee2023-01-25 21:51:42 +00005582 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.nocommit,
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005583 options.force, options.newbranch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005584
5585
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005586def GetTreeStatus(url=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005587 """Fetches the tree status and returns either 'open', 'closed',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005588 'unknown' or 'unset'."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005589 url = url or settings.GetTreeStatusUrl(error_ok=True)
5590 if url:
5591 status = str(urllib.request.urlopen(url).read().lower())
5592 if status.find('closed') != -1 or status == '0':
5593 return 'closed'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00005594
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005595 if status.find('open') != -1 or status == '1':
5596 return 'open'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00005597
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005598 return 'unknown'
5599 return 'unset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005600
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005601
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005602def GetTreeStatusReason():
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005603 """Fetches the tree status from a json url and returns the message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005604 with the reason for the tree to be opened or closed."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005605 url = settings.GetTreeStatusUrl()
5606 json_url = urllib.parse.urljoin(url, '/current?format=json')
5607 connection = urllib.request.urlopen(json_url)
5608 status = json.loads(connection.read())
5609 connection.close()
5610 return status['message']
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005611
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005612
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005613@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005614def CMDtree(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005615 """Shows the status of the tree."""
5616 _, args = parser.parse_args(args)
5617 status = GetTreeStatus()
5618 if 'unset' == status:
5619 print(
5620 'You must configure your tree status URL by running "git cl config".'
5621 )
5622 return 2
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005623
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005624 print('The tree is %s' % status)
5625 print()
5626 print(GetTreeStatusReason())
5627 if status != 'open':
5628 return 1
5629 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005630
5631
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005632@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00005633def CMDtry(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005634 """Triggers tryjobs using either Buildbucket or CQ dry run."""
5635 group = optparse.OptionGroup(parser, 'Tryjob options')
5636 group.add_option(
5637 '-b',
5638 '--bot',
5639 action='append',
5640 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5641 'times to specify multiple builders. ex: '
5642 '"-b win_rel -b win_layout". See '
5643 'the try server waterfall for the builders name and the tests '
5644 'available.'))
5645 group.add_option(
5646 '-B',
5647 '--bucket',
5648 default='',
5649 help=('Buildbucket bucket to send the try requests. Format: '
5650 '"luci.$LUCI_PROJECT.$LUCI_BUCKET". eg: "luci.chromium.try"'))
5651 group.add_option(
5652 '-r',
5653 '--revision',
5654 help='Revision to use for the tryjob; default: the revision will '
5655 'be determined by the try recipe that builder runs, which usually '
5656 'defaults to HEAD of origin/master or origin/main')
5657 group.add_option(
5658 '-c',
5659 '--clobber',
5660 action='store_true',
5661 default=False,
5662 help='Force a clobber before building; that is don\'t do an '
5663 'incremental build')
5664 group.add_option('--category',
5665 default='git_cl_try',
5666 help='Specify custom build category.')
5667 group.add_option(
5668 '--project',
5669 help='Override which project to use. Projects are defined '
5670 'in recipe to determine to which repository or directory to '
5671 'apply the patch')
5672 group.add_option(
5673 '-p',
5674 '--property',
5675 dest='properties',
5676 action='append',
5677 default=[],
5678 help='Specify generic properties in the form -p key1=value1 -p '
5679 'key2=value2 etc. The value will be treated as '
5680 'json if decodable, or as string otherwise. '
5681 'NOTE: using this may make your tryjob not usable for CQ, '
5682 'which will then schedule another tryjob with default properties')
5683 group.add_option('--buildbucket-host',
5684 default='cr-buildbucket.appspot.com',
5685 help='Host of buildbucket. The default host is %default.')
5686 parser.add_option_group(group)
5687 parser.add_option('-R',
5688 '--retry-failed',
5689 action='store_true',
5690 default=False,
5691 help='Retry failed jobs from the latest set of tryjobs. '
5692 'Not allowed with --bucket and --bot options.')
5693 parser.add_option(
5694 '-i',
5695 '--issue',
5696 type=int,
5697 help='Operate on this issue instead of the current branch\'s implicit '
5698 'issue.')
5699 options, args = parser.parse_args(args)
maruel@chromium.org15192402012-09-06 12:38:29 +00005700
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005701 # Make sure that all properties are prop=value pairs.
5702 bad_params = [x for x in options.properties if '=' not in x]
5703 if bad_params:
5704 parser.error('Got properties with missing "=": %s' % bad_params)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005705
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005706 if args:
5707 parser.error('Unknown arguments: %s' % args)
maruel@chromium.org15192402012-09-06 12:38:29 +00005708
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005709 cl = Changelist(issue=options.issue)
5710 if not cl.GetIssue():
5711 parser.error('Need to upload first.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005712
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005713 # HACK: warm up Gerrit change detail cache to save on RPCs.
5714 cl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005715
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005716 error_message = cl.CannotTriggerTryJobReason()
5717 if error_message:
5718 parser.error('Can\'t trigger tryjobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005719
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005720 if options.bot:
5721 if options.retry_failed:
5722 parser.error('--bot is not compatible with --retry-failed.')
5723 if not options.bucket:
5724 parser.error('A bucket (e.g. "chromium/try") is required.')
Edward Lemur45768512020-03-02 19:03:14 +00005725
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005726 triggered = [b for b in options.bot if 'triggered' in b]
5727 if triggered:
5728 parser.error(
5729 'Cannot schedule builds on triggered bots: %s.\n'
5730 'This type of bot requires an initial job from a parent (usually a '
5731 'builder). Schedule a job on the parent instead.\n' % triggered)
Edward Lemur45768512020-03-02 19:03:14 +00005732
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005733 if options.bucket.startswith('.master'):
5734 parser.error('Buildbot masters are not supported.')
Edward Lemur45768512020-03-02 19:03:14 +00005735
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005736 project, bucket = _parse_bucket(options.bucket)
5737 if project is None or bucket is None:
5738 parser.error('Invalid bucket: %s.' % options.bucket)
5739 jobs = sorted((project, bucket, bot) for bot in options.bot)
5740 elif options.retry_failed:
5741 print('Searching for failed tryjobs...')
5742 builds, patchset = _fetch_latest_builds(cl, DEFAULT_BUILDBUCKET_HOST)
5743 if options.verbose:
5744 print('Got %d builds in patchset #%d' % (len(builds), patchset))
5745 jobs = _filter_failed_for_retry(builds)
5746 if not jobs:
5747 print('There are no failed jobs in the latest set of jobs '
5748 '(patchset #%d), doing nothing.' % patchset)
5749 return 0
5750 num_builders = len(jobs)
5751 if num_builders > 10:
5752 confirm_or_exit('There are %d builders with failed builds.' %
5753 num_builders,
5754 action='continue')
5755 else:
5756 if options.verbose:
5757 print('git cl try with no bots now defaults to CQ dry run.')
5758 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5759 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005760
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005761 patchset = cl.GetMostRecentPatchset()
5762 try:
5763 _trigger_tryjobs(cl, jobs, options, patchset)
5764 except BuildbucketResponseException as ex:
5765 print('ERROR: %s' % ex)
5766 return 1
5767 return 0
maruel@chromium.org15192402012-09-06 12:38:29 +00005768
5769
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005770@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005771def CMDtry_results(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005772 """Prints info about results for tryjobs associated with the current CL."""
5773 group = optparse.OptionGroup(parser, 'Tryjob results options')
5774 group.add_option('-p',
5775 '--patchset',
5776 type=int,
5777 help='patchset number if not current.')
5778 group.add_option('--print-master',
5779 action='store_true',
5780 help='print master name as well.')
5781 group.add_option('--color',
5782 action='store_true',
5783 default=setup_color.IS_TTY,
5784 help='force color output, useful when piping output.')
5785 group.add_option('--buildbucket-host',
5786 default='cr-buildbucket.appspot.com',
5787 help='Host of buildbucket. The default host is %default.')
5788 group.add_option(
5789 '--json',
5790 help=('Path of JSON output file to write tryjob results to,'
5791 'or "-" for stdout.'))
5792 parser.add_option_group(group)
5793 parser.add_option(
5794 '-i',
5795 '--issue',
5796 type=int,
5797 help='Operate on this issue instead of the current branch\'s implicit '
5798 'issue.')
5799 options, args = parser.parse_args(args)
5800 if args:
5801 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005802
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005803 cl = Changelist(issue=options.issue)
5804 if not cl.GetIssue():
5805 parser.error('Need to upload first.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005806
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005807 patchset = options.patchset
tandrii221ab252016-10-06 08:12:04 -07005808 if not patchset:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005809 patchset = cl.GetMostRecentDryRunPatchset()
5810 if not patchset:
5811 parser.error('Code review host doesn\'t know about issue %s. '
5812 'No access to issue or wrong issue number?\n'
5813 'Either upload first, or pass --patchset explicitly.' %
5814 cl.GetIssue())
tandrii221ab252016-10-06 08:12:04 -07005815
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005816 try:
5817 jobs = _fetch_tryjobs(cl, DEFAULT_BUILDBUCKET_HOST, patchset)
5818 except BuildbucketResponseException as ex:
5819 print('Buildbucket error: %s' % ex)
5820 return 1
5821 if options.json:
5822 write_json(options.json, jobs)
5823 else:
5824 _print_tryjobs(options, jobs)
5825 return 0
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005826
5827
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005828@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005829@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005830def CMDupstream(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005831 """Prints or sets the name of the upstream branch, if any."""
5832 _, args = parser.parse_args(args)
5833 if len(args) > 1:
5834 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005835
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005836 cl = Changelist()
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005837 if args:
5838 # One arg means set upstream branch.
5839 branch = cl.GetBranch()
5840 RunGit(['branch', '--set-upstream-to', args[0], branch])
5841 cl = Changelist()
5842 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(), ))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005843
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005844 # Clear configured merge-base, if there is one.
5845 git_common.remove_merge_base(branch)
5846 else:
5847 print(cl.GetUpstreamBranch())
5848 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005849
5850
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005851@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00005852def CMDweb(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005853 """Opens the current CL in the web browser."""
5854 parser.add_option('-p',
5855 '--print-only',
5856 action='store_true',
5857 dest='print_only',
5858 help='Only print the Gerrit URL, don\'t open it in the '
5859 'browser.')
5860 (options, args) = parser.parse_args(args)
5861 if args:
5862 parser.error('Unrecognized args: %s' % ' '.join(args))
thestig@chromium.org00858c82013-12-02 23:08:03 +00005863
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005864 issue_url = Changelist().GetIssueURL()
5865 if not issue_url:
5866 print('ERROR No issue to open', file=sys.stderr)
5867 return 1
thestig@chromium.org00858c82013-12-02 23:08:03 +00005868
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005869 if options.print_only:
5870 print(issue_url)
5871 return 0
5872
5873 # Redirect I/O before invoking browser to hide its output. For example, this
5874 # allows us to hide the "Created new window in existing browser session."
5875 # message from Chrome. Based on https://stackoverflow.com/a/2323563.
5876 saved_stdout = os.dup(1)
5877 saved_stderr = os.dup(2)
5878 os.close(1)
5879 os.close(2)
5880 os.open(os.devnull, os.O_RDWR)
5881 try:
5882 webbrowser.open(issue_url)
5883 finally:
5884 os.dup2(saved_stdout, 1)
5885 os.dup2(saved_stderr, 2)
Orr Bernstein0b960582022-12-22 20:16:18 +00005886 return 0
5887
thestig@chromium.org00858c82013-12-02 23:08:03 +00005888
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005889@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005890def CMDset_commit(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005891 """Sets the commit bit to trigger the CQ."""
5892 parser.add_option('-d',
5893 '--dry-run',
5894 action='store_true',
5895 help='trigger in dry run mode')
5896 parser.add_option('-c',
5897 '--clear',
5898 action='store_true',
5899 help='stop CQ run, if any')
5900 parser.add_option(
5901 '-i',
5902 '--issue',
5903 type=int,
5904 help='Operate on this issue instead of the current branch\'s implicit '
5905 'issue.')
5906 options, args = parser.parse_args(args)
5907 if args:
5908 parser.error('Unrecognized args: %s' % ' '.join(args))
5909 if [options.dry_run, options.clear].count(True) > 1:
5910 parser.error('Only one of --dry-run, and --clear are allowed.')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005911
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005912 cl = Changelist(issue=options.issue)
5913 if not cl.GetIssue():
5914 parser.error('Must upload the issue first.')
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00005915
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005916 if options.clear:
5917 state = _CQState.NONE
5918 elif options.dry_run:
5919 state = _CQState.DRY_RUN
5920 else:
5921 state = _CQState.COMMIT
5922 cl.SetCQState(state)
5923 return 0
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005924
5925
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005926@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005927def CMDset_close(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005928 """Closes the issue."""
5929 parser.add_option(
5930 '-i',
5931 '--issue',
5932 type=int,
5933 help='Operate on this issue instead of the current branch\'s implicit '
5934 'issue.')
5935 options, args = parser.parse_args(args)
5936 if args:
5937 parser.error('Unrecognized args: %s' % ' '.join(args))
5938 cl = Changelist(issue=options.issue)
5939 # Ensure there actually is an issue to close.
5940 if not cl.GetIssue():
5941 DieWithError('ERROR: No issue to close.')
5942 cl.CloseIssue()
5943 return 0
groby@chromium.org411034a2013-02-26 15:12:01 +00005944
5945
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005946@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005947def CMDdiff(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005948 """Shows differences between local tree and last upload."""
5949 parser.add_option('--stat',
5950 action='store_true',
5951 dest='stat',
5952 help='Generate a diffstat')
5953 options, args = parser.parse_args(args)
5954 if args:
5955 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005956
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005957 cl = Changelist()
5958 issue = cl.GetIssue()
5959 branch = cl.GetBranch()
5960 if not issue:
5961 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005962
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005963 base = cl._GitGetBranchConfigValue(LAST_UPLOAD_HASH_CONFIG_KEY)
5964 if not base:
5965 base = cl._GitGetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY)
5966 if not base:
5967 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5968 revision_info = detail['revisions'][detail['current_revision']]
5969 fetch_info = revision_info['fetch']['http']
5970 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5971 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005972
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005973 cmd = ['git', 'diff']
5974 if options.stat:
5975 cmd.append('--stat')
5976 cmd.append(base)
5977 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005978
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005979 return 0
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005980
5981
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005982@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005983def CMDowners(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005984 """Finds potential owners for reviewing."""
5985 parser.add_option(
5986 '--ignore-current',
5987 action='store_true',
5988 help='Ignore the CL\'s current reviewers and start from scratch.')
5989 parser.add_option('--ignore-self',
5990 action='store_true',
5991 help='Do not consider CL\'s author as an owners.')
5992 parser.add_option('--no-color',
5993 action='store_true',
5994 help='Use this option to disable color output')
5995 parser.add_option('--batch',
5996 action='store_true',
5997 help='Do not run interactively, just suggest some')
5998 # TODO: Consider moving this to another command, since other
5999 # git-cl owners commands deal with owners for a given CL.
6000 parser.add_option('--show-all',
6001 action='store_true',
6002 help='Show all owners for a particular file')
6003 options, args = parser.parse_args(args)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00006004
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006005 cl = Changelist()
6006 author = cl.GetAuthor()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00006007
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006008 if options.show_all:
6009 if len(args) == 0:
6010 print('No files specified for --show-all. Nothing to do.')
6011 return 0
6012 owners_by_path = cl.owners_client.BatchListOwners(args)
6013 for path in args:
6014 print('Owners for %s:' % path)
6015 print('\n'.join(
6016 ' - %s' % owner
6017 for owner in owners_by_path.get(path, ['No owners found'])))
6018 return 0
Yang Guo6e269a02019-06-26 11:17:02 +00006019
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006020 if args:
6021 if len(args) > 1:
6022 parser.error('Unknown args.')
6023 base_branch = args[0]
6024 else:
6025 # Default to diffing against the common ancestor of the upstream branch.
6026 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00006027
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006028 affected_files = cl.GetAffectedFiles(base_branch)
Dirk Prankebf980882017-09-02 15:08:00 -07006029
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006030 if options.batch:
6031 owners = cl.owners_client.SuggestOwners(affected_files,
6032 exclude=[author])
6033 print('\n'.join(owners))
6034 return 0
Dirk Prankebf980882017-09-02 15:08:00 -07006035
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006036 return owners_finder.OwnersFinder(
6037 affected_files,
6038 author, [] if options.ignore_current else cl.GetReviewers(),
6039 cl.owners_client,
6040 disable_color=options.no_color,
6041 ignore_author=options.ignore_self).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00006042
6043
Aiden Bennerc08566e2018-10-03 17:52:42 +00006044def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006045 """Generates a diff command."""
6046 # Generate diff for the current branch's changes.
6047 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
Aiden Bennerc08566e2018-10-03 17:52:42 +00006048
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006049 if allow_prefix:
6050 # explicitly setting --src-prefix and --dst-prefix is necessary in the
6051 # case that diff.noprefix is set in the user's git config.
6052 diff_cmd += ['--src-prefix=a/', '--dst-prefix=b/']
6053 else:
6054 diff_cmd += ['--no-prefix']
Aiden Bennerc08566e2018-10-03 17:52:42 +00006055
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006056 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006057
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006058 if args:
6059 for arg in args:
6060 if os.path.isdir(arg) or os.path.isfile(arg):
6061 diff_cmd.append(arg)
6062 else:
6063 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006064
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006065 return diff_cmd
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006066
Andrii Shyshkalov18975322017-01-25 16:44:13 +01006067
Jamie Madill5e96ad12020-01-13 16:08:35 +00006068def _RunClangFormatDiff(opts, clang_diff_files, top_dir, upstream_commit):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006069 """Runs clang-format-diff and sets a return value if necessary."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006070 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
6071 # formatted. This is used to block during the presubmit.
6072 return_value = 0
Jamie Madill5e96ad12020-01-13 16:08:35 +00006073
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006074 # Locate the clang-format binary in the checkout
Jamie Madill5e96ad12020-01-13 16:08:35 +00006075 try:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006076 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
Jamie Madill5e96ad12020-01-13 16:08:35 +00006077 except clang_format.NotFoundError as e:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006078 DieWithError(e)
Jamie Madill5e96ad12020-01-13 16:08:35 +00006079
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006080 if opts.full or settings.GetFormatFullByDefault():
6081 cmd = [clang_format_tool]
6082 if not opts.dry_run and not opts.diff:
6083 cmd.append('-i')
6084 if opts.dry_run:
6085 for diff_file in clang_diff_files:
6086 with open(diff_file, 'r') as myfile:
6087 code = myfile.read().replace('\r\n', '\n')
6088 stdout = RunCommand(cmd + [diff_file], cwd=top_dir)
6089 stdout = stdout.replace('\r\n', '\n')
6090 if opts.diff:
6091 sys.stdout.write(stdout)
6092 if code != stdout:
6093 return_value = 2
6094 else:
6095 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
6096 if opts.diff:
6097 sys.stdout.write(stdout)
6098 else:
6099 try:
6100 script = clang_format.FindClangFormatScriptInChromiumTree(
6101 'clang-format-diff.py')
6102 except clang_format.NotFoundError as e:
6103 DieWithError(e)
Jamie Madill5e96ad12020-01-13 16:08:35 +00006104
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006105 cmd = ['vpython3', script, '-p0']
6106 if not opts.dry_run and not opts.diff:
6107 cmd.append('-i')
Jamie Madill5e96ad12020-01-13 16:08:35 +00006108
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006109 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
6110 diff_output = RunGit(diff_cmd).encode('utf-8')
Jamie Madill5e96ad12020-01-13 16:08:35 +00006111
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006112 env = os.environ.copy()
6113 env['PATH'] = (str(os.path.dirname(clang_format_tool)) + os.pathsep +
6114 env['PATH'])
6115 stdout = RunCommand(cmd,
6116 stdin=diff_output,
6117 cwd=top_dir,
6118 env=env,
6119 shell=sys.platform.startswith('win32'))
6120 if opts.diff:
6121 sys.stdout.write(stdout)
6122 if opts.dry_run and len(stdout) > 0:
6123 return_value = 2
6124
6125 return return_value
Jamie Madill5e96ad12020-01-13 16:08:35 +00006126
6127
Andrew Grieved7ba85d2023-09-15 18:28:33 +00006128def _FindGoogleJavaFormat():
6129 primary_solution_path = gclient_paths.GetPrimarySolutionPath()
6130 if primary_solution_path:
6131 path = os.path.join(primary_solution_path, 'third_party',
6132 'google-java-format', 'google-java-format')
6133 if os.path.exists(path):
6134 return path
6135
6136 return shutil.which('google-java-format')
6137
6138
6139def _RunGoogleJavaFormat(opts, paths, top_dir, upstream_commit):
6140 """Runs google-java-format and sets a return value if necessary."""
6141 google_java_format = _FindGoogleJavaFormat()
6142 if google_java_format is None:
6143 DieWithError('Could not find google-java-format.')
6144
6145 base_cmd = [google_java_format, '--aosp']
6146 if opts.dry_run or opts.diff:
6147 base_cmd += ['--dry-run']
6148 else:
6149 base_cmd += ['--replace']
6150
6151 changed_lines_only = not (opts.full or settings.GetFormatFullByDefault())
6152 if changed_lines_only:
6153 line_diffs = _ComputeFormatDiffLineRanges(paths, upstream_commit)
6154
6155 results = []
6156 kwds = {'error_ok': True, 'cwd': top_dir}
6157 with multiprocessing.pool.ThreadPool() as pool:
6158 for path in paths:
6159 cmd = base_cmd.copy()
6160 if changed_lines_only:
6161 ranges = line_diffs.get(path)
6162 if not ranges:
6163 # E.g. There were only deleted lines.
6164 continue
6165 cmd.extend('--lines={}:{}'.format(a, b) for a, b in ranges)
6166
6167 results.append(
6168 pool.apply_async(RunCommand, args=[cmd + [path]], kwds=kwds))
6169
6170 return_value = 0
6171 for result in results:
6172 stdout = result.get()
6173 if stdout:
6174 if opts.diff:
6175 sys.stdout.write('Requires formatting: ' + stdout)
6176 else:
6177 return_value = 2
6178
6179 return return_value
6180
6181
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006182def _RunRustFmt(opts, rust_diff_files, top_dir, upstream_commit):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006183 """Runs rustfmt. Just like _RunClangFormatDiff returns 2 to indicate that
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006184 presubmit checks have failed (and returns 0 otherwise)."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006185 # Locate the rustfmt binary.
6186 try:
6187 rustfmt_tool = rustfmt.FindRustfmtToolInChromiumTree()
6188 except rustfmt.NotFoundError as e:
6189 DieWithError(e)
6190
6191 # TODO(crbug.com/1440869): Support formatting only the changed lines
6192 # if `opts.full or settings.GetFormatFullByDefault()` is False.
6193 cmd = [rustfmt_tool]
6194 if opts.dry_run:
6195 cmd.append('--check')
6196 cmd += rust_diff_files
6197 rustfmt_exitcode = subprocess2.call(cmd)
6198
6199 if opts.presubmit and rustfmt_exitcode != 0:
6200 return 2
6201
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006202 return 0
6203
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006204
Olivier Robin0a6b5442022-04-07 07:25:04 +00006205def _RunSwiftFormat(opts, swift_diff_files, top_dir, upstream_commit):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006206 """Runs swift-format. Just like _RunClangFormatDiff returns 2 to indicate
Olivier Robin0a6b5442022-04-07 07:25:04 +00006207 that presubmit checks have failed (and returns 0 otherwise)."""
Andrew Grievecca48db2023-09-14 14:12:23 +00006208 if sys.platform != 'darwin':
6209 DieWithError('swift-format is only supported on macOS.')
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006210 # Locate the swift-format binary.
6211 try:
6212 swift_format_tool = swift_format.FindSwiftFormatToolInChromiumTree()
6213 except swift_format.NotFoundError as e:
6214 DieWithError(e)
6215
6216 cmd = [swift_format_tool]
6217 if opts.dry_run:
6218 cmd += ['lint', '-s']
6219 else:
6220 cmd += ['format', '-i']
6221 cmd += swift_diff_files
6222 swift_format_exitcode = subprocess2.call(cmd)
6223
6224 if opts.presubmit and swift_format_exitcode != 0:
6225 return 2
6226
Olivier Robin0a6b5442022-04-07 07:25:04 +00006227 return 0
6228
Olivier Robin0a6b5442022-04-07 07:25:04 +00006229
Andrew Grievecca48db2023-09-14 14:12:23 +00006230def _RunYapf(opts, paths, top_dir, upstream_commit):
6231 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
6232 yapf_tool = os.path.join(depot_tools_path, 'yapf')
6233
6234 # Used for caching.
6235 yapf_configs = {}
6236 for p in paths:
6237 # Find the yapf style config for the current file, defaults to depot
6238 # tools default.
6239 _FindYapfConfigFile(p, yapf_configs, top_dir)
6240
6241 # Turn on python formatting by default if a yapf config is specified.
6242 # This breaks in the case of this repo though since the specified
6243 # style file is also the global default.
6244 if opts.python is None:
6245 paths = [
6246 p for p in paths
6247 if _FindYapfConfigFile(p, yapf_configs, top_dir) is not None
6248 ]
6249
6250 # Note: yapf still seems to fix indentation of the entire file
6251 # even if line ranges are specified.
6252 # See https://github.com/google/yapf/issues/499
6253 if not opts.full and paths:
Andrew Grieved7ba85d2023-09-15 18:28:33 +00006254 line_diffs = _ComputeFormatDiffLineRanges(paths, upstream_commit)
Andrew Grievecca48db2023-09-14 14:12:23 +00006255
6256 yapfignore_patterns = _GetYapfIgnorePatterns(top_dir)
6257 paths = _FilterYapfIgnoredFiles(paths, yapfignore_patterns)
6258
6259 return_value = 0
6260 for path in paths:
6261 yapf_style = _FindYapfConfigFile(path, yapf_configs, top_dir)
6262 # Default to pep8 if not .style.yapf is found.
6263 if not yapf_style:
6264 yapf_style = 'pep8'
6265
6266 with open(path, 'r') as py_f:
6267 if 'python2' in py_f.readline():
6268 vpython_script = 'vpython'
6269 else:
6270 vpython_script = 'vpython3'
6271
6272 cmd = [vpython_script, yapf_tool, '--style', yapf_style, path]
6273
Andrew Grievecca48db2023-09-14 14:12:23 +00006274 if not opts.full:
Andrew Grieved7ba85d2023-09-15 18:28:33 +00006275 ranges = line_diffs.get(path)
6276 if not ranges:
Andrew Grievecca48db2023-09-14 14:12:23 +00006277 continue
Andrew Grieved7ba85d2023-09-15 18:28:33 +00006278 # Only run yapf over changed line ranges.
6279 for diff_start, diff_end in ranges:
6280 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
Andrew Grievecca48db2023-09-14 14:12:23 +00006281
6282 if opts.diff or opts.dry_run:
6283 cmd += ['--diff']
6284 # Will return non-zero exit code if non-empty diff.
6285 stdout = RunCommand(cmd,
6286 error_ok=True,
6287 stderr=subprocess2.PIPE,
6288 cwd=top_dir,
6289 shell=sys.platform.startswith('win32'))
6290 if opts.diff:
6291 sys.stdout.write(stdout)
6292 elif len(stdout) > 0:
6293 return_value = 2
6294 else:
6295 cmd += ['-i']
6296 RunCommand(cmd, cwd=top_dir, shell=sys.platform.startswith('win32'))
6297 return return_value
6298
6299
6300def _RunGnFormat(opts, paths, top_dir, upstream_commit):
6301 cmd = ['gn', 'format']
6302 if opts.dry_run or opts.diff:
6303 cmd.append('--dry-run')
6304 return_value = 0
6305 for path in paths:
6306 gn_ret = subprocess2.call(cmd + [path],
6307 shell=sys.platform.startswith('win'),
6308 cwd=top_dir)
6309 if opts.dry_run and gn_ret == 2:
6310 return_value = 2 # Not formatted.
6311 elif opts.diff and gn_ret == 2:
6312 # TODO this should compute and print the actual diff.
6313 print('This change has GN build file diff for ' + path)
6314 elif gn_ret != 0:
6315 # For non-dry run cases (and non-2 return values for dry-run), a
6316 # nonzero error code indicates a failure, probably because the
6317 # file doesn't parse.
6318 DieWithError('gn format failed on ' + path +
6319 '\nTry running `gn format` on this file manually.')
6320 return return_value
6321
6322
6323def _FormatXml(opts, paths, top_dir, upstream_commit):
6324 # Skip the metrics formatting from the global presubmit hook. These files
6325 # have a separate presubmit hook that issues an error if the files need
6326 # formatting, whereas the top-level presubmit script merely issues a
6327 # warning. Formatting these files is somewhat slow, so it's important not to
6328 # duplicate the work.
6329 if opts.presubmit:
6330 return 0
6331
6332 return_value = 0
6333 for path in paths:
6334 xml_dir = GetMetricsDir(path)
6335 if not xml_dir:
6336 continue
6337
6338 tool_dir = os.path.join(top_dir, xml_dir)
6339 pretty_print_tool = os.path.join(tool_dir, 'pretty_print.py')
6340 cmd = [shutil.which('vpython3'), pretty_print_tool, '--non-interactive']
6341
6342 # If the XML file is histograms.xml or enums.xml, add the xml path
6343 # to the command as histograms/pretty_print.py now needs a relative
6344 # path argument after splitting the histograms into multiple
6345 # directories. For example, in tools/metrics/ukm, pretty-print could
6346 # be run using: $ python pretty_print.py But in
6347 # tools/metrics/histogrmas, pretty-print should be run with an
6348 # additional relative path argument, like: $ python pretty_print.py
6349 # metadata/UMA/histograms.xml $ python pretty_print.py enums.xml
6350 if xml_dir == os.path.join('tools', 'metrics', 'histograms'):
6351 if os.path.basename(path) not in ('histograms.xml', 'enums.xml',
6352 'histogram_suffixes_list.xml'):
6353 # Skip this XML file if it's not one of the known types.
6354 continue
6355 cmd.append(path)
6356
6357 if opts.dry_run or opts.diff:
6358 cmd.append('--diff')
6359
6360 stdout = RunCommand(cmd, cwd=top_dir)
6361 if opts.diff:
6362 sys.stdout.write(stdout)
6363 if opts.dry_run and stdout:
6364 return_value = 2 # Not formatted.
6365 return return_value
6366
6367
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006368def MatchingFileType(file_name, extensions):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006369 """Returns True if the file name ends with one of the given extensions."""
6370 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006371
Andrii Shyshkalov18975322017-01-25 16:44:13 +01006372
enne@chromium.org555cfe42014-01-29 18:21:39 +00006373@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006374@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006375def CMDformat(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006376 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Andrew Grievecca48db2023-09-14 14:12:23 +00006377 clang_exts = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006378 GN_EXTS = ['.gn', '.gni', '.typemap']
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006379 parser.add_option('--full',
6380 action='store_true',
6381 help='Reformat the full content of all touched files')
6382 parser.add_option('--upstream', help='Branch to check against')
6383 parser.add_option('--dry-run',
6384 action='store_true',
6385 help='Don\'t modify any file on disk.')
6386 parser.add_option(
6387 '--no-clang-format',
6388 dest='clang_format',
6389 action='store_false',
6390 default=True,
6391 help='Disables formatting of various file types using clang-format.')
6392 parser.add_option('--python',
6393 action='store_true',
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006394 help='Enables python formatting on all python files.')
6395 parser.add_option(
6396 '--no-python',
Andrew Grievecca48db2023-09-14 14:12:23 +00006397 action='store_false',
6398 dest='python',
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006399 help='Disables python formatting on all python files. '
6400 'If neither --python or --no-python are set, python files that have a '
6401 '.style.yapf file in an ancestor directory will be formatted. '
6402 'It is an error to set both.')
6403 parser.add_option('--js',
6404 action='store_true',
6405 help='Format javascript code with clang-format. '
6406 'Has no effect if --no-clang-format is set.')
6407 parser.add_option('--diff',
6408 action='store_true',
6409 help='Print diff to stdout rather than modifying files.')
6410 parser.add_option('--presubmit',
6411 action='store_true',
6412 help='Used when running the script from a presubmit.')
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006413
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006414 parser.add_option(
6415 '--rust-fmt',
6416 dest='use_rust_fmt',
6417 action='store_true',
6418 default=rustfmt.IsRustfmtSupported(),
6419 help='Enables formatting of Rust file types using rustfmt.')
6420 parser.add_option(
6421 '--no-rust-fmt',
6422 dest='use_rust_fmt',
6423 action='store_false',
6424 help='Disables formatting of Rust file types using rustfmt.')
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006425
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006426 parser.add_option(
6427 '--swift-format',
6428 dest='use_swift_format',
6429 action='store_true',
6430 default=swift_format.IsSwiftFormatSupported(),
6431 help='Enables formatting of Swift file types using swift-format '
6432 '(macOS host only).')
6433 parser.add_option(
6434 '--no-swift-format',
6435 dest='use_swift_format',
6436 action='store_false',
6437 help='Disables formatting of Swift file types using swift-format.')
Olivier Robin0a6b5442022-04-07 07:25:04 +00006438
Andrew Grieved7ba85d2023-09-15 18:28:33 +00006439 # Temporary flag to test with google-java-format.
6440 parser.add_option('--google-java-format',
6441 action='store_true',
6442 help=optparse.SUPPRESS_HELP)
6443
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006444 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006445
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006446 # Normalize any remaining args against the current path, so paths relative
6447 # to the current directory are still resolved as expected.
6448 args = [os.path.join(os.getcwd(), arg) for arg in args]
Daniel Chengc55eecf2016-12-30 03:11:02 -08006449
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006450 # git diff generates paths against the root of the repository. Change
6451 # to that directory so clang-format can find files even within subdirs.
6452 rel_base_path = settings.GetRelativeRoot()
6453 if rel_base_path:
6454 os.chdir(rel_base_path)
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00006455
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006456 # Grab the merge-base commit, i.e. the upstream commit of the current
6457 # branch when it was created or the last time it was rebased. This is
6458 # to cover the case where the user may have called "git fetch origin",
6459 # moving the origin branch to a newer commit, but hasn't rebased yet.
6460 upstream_commit = None
6461 upstream_branch = opts.upstream
6462 if not upstream_branch:
6463 cl = Changelist()
6464 upstream_branch = cl.GetUpstreamBranch()
6465 if upstream_branch:
6466 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
6467 upstream_commit = upstream_commit.strip()
digit@chromium.org29e47272013-05-17 17:01:46 +00006468
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006469 if not upstream_commit:
6470 DieWithError('Could not find base commit for this branch. '
6471 'Are you in detached state?')
digit@chromium.org29e47272013-05-17 17:01:46 +00006472
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006473 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
6474 diff_output = RunGit(changed_files_cmd)
6475 diff_files = diff_output.splitlines()
6476 # Filter out files deleted by this CL
6477 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006478
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006479 if opts.js:
Andrew Grievecca48db2023-09-14 14:12:23 +00006480 clang_exts.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11006481
Andrew Grievecca48db2023-09-14 14:12:23 +00006482 formatters = [
6483 (GN_EXTS, _RunGnFormat),
6484 (['.xml'], _FormatXml),
Garrett Beatyed0cc5f2020-01-06 17:26:13 +00006485 ]
Andrew Grieved7ba85d2023-09-15 18:28:33 +00006486 if opts.google_java_format:
6487 clang_exts.remove('.java')
6488 formatters += [(['.java'], _RunGoogleJavaFormat)]
Andrew Grievecca48db2023-09-14 14:12:23 +00006489 if opts.clang_format:
6490 formatters += [(clang_exts, _RunClangFormatDiff)]
6491 if opts.use_rust_fmt:
6492 formatters += [(['.rs'], _RunRustFmt)]
6493 if opts.use_swift_format:
6494 formatters += [(['.swift'], _RunSwiftFormat)]
6495 if opts.python is not False:
6496 formatters += [(['.py'], _RunYapf)]
digit@chromium.org29e47272013-05-17 17:01:46 +00006497
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006498 top_dir = settings.GetRoot()
Andrew Grievecca48db2023-09-14 14:12:23 +00006499 return_value = 0
6500 for file_types, format_func in formatters:
6501 paths = [p for p in diff_files if MatchingFileType(p, file_types)]
6502 if not paths:
6503 continue
6504 ret = format_func(opts, paths, top_dir, upstream_commit)
6505 return_value = return_value or ret
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006506
6507 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006508
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00006509
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006510def GetMetricsDir(diff_xml):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006511 metrics_xml_dirs = [
6512 os.path.join('tools', 'metrics', 'actions'),
6513 os.path.join('tools', 'metrics', 'histograms'),
6514 os.path.join('tools', 'metrics', 'structured'),
6515 os.path.join('tools', 'metrics', 'ukm'),
6516 ]
6517 for xml_dir in metrics_xml_dirs:
6518 if diff_xml.startswith(xml_dir):
6519 return xml_dir
6520 return None
Steven Holte2e664bf2017-04-21 13:10:47 -07006521
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006522
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006523@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006524@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006525def CMDcheckout(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006526 """Checks out a branch associated with a given Gerrit issue."""
6527 _, args = parser.parse_args(args)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006528
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006529 if len(args) != 1:
6530 parser.print_help()
6531 return 1
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006532
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006533 issue_arg = ParseIssueNumberArgument(args[0])
6534 if not issue_arg.valid:
6535 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02006536
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006537 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006538
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006539 output = RunGit([
6540 'config', '--local', '--get-regexp', r'branch\..*\.' + ISSUE_CONFIG_KEY
6541 ],
6542 error_ok=True)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006543
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006544 branches = []
6545 for key, issue in [x.split() for x in output.splitlines()]:
6546 if issue == target_issue:
6547 branches.append(
6548 re.sub(r'branch\.(.*)\.' + ISSUE_CONFIG_KEY, r'\1', key))
Edward Lemur52969c92020-02-06 18:15:28 +00006549
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006550 if len(branches) == 0:
6551 print('No branch found for issue %s.' % target_issue)
6552 return 1
6553 if len(branches) == 1:
6554 RunGit(['checkout', branches[0]])
6555 else:
6556 print('Multiple branches match issue %s:' % target_issue)
6557 for i in range(len(branches)):
6558 print('%d: %s' % (i, branches[i]))
6559 which = gclient_utils.AskForData('Choose by index: ')
6560 try:
6561 RunGit(['checkout', branches[int(which)]])
6562 except (IndexError, ValueError):
6563 print('Invalid selection, not checking out any branch.')
6564 return 1
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006565
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006566 return 0
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006567
6568
maruel@chromium.org29404b52014-09-08 22:58:00 +00006569def CMDlol(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006570 # This command is intentionally undocumented.
6571 print(
6572 zlib.decompress(
6573 base64.b64decode(
6574 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
6575 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
6576 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
6577 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')).decode('utf-8'))
6578 return 0
maruel@chromium.org29404b52014-09-08 22:58:00 +00006579
6580
Josip Sokcevic0399e172022-03-21 23:11:51 +00006581def CMDversion(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006582 import utils
6583 print(utils.depot_tools_version())
Josip Sokcevic0399e172022-03-21 23:11:51 +00006584
6585
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006586class OptionParser(optparse.OptionParser):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006587 """Creates the option parse and add --verbose support."""
6588 def __init__(self, *args, **kwargs):
6589 optparse.OptionParser.__init__(self,
6590 *args,
6591 prog='git cl',
6592 version=__version__,
6593 **kwargs)
6594 self.add_option('-v',
6595 '--verbose',
6596 action='count',
6597 default=0,
6598 help='Use 2 times for more debugging info')
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00006599
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006600 def parse_args(self, args=None, _values=None):
Joanna Wangc5b38322023-03-15 20:38:46 +00006601 try:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006602 return self._parse_args(args)
6603 finally:
6604 # Regardless of success or failure of args parsing, we want to
6605 # report metrics, but only after logging has been initialized (if
6606 # parsing succeeded).
6607 global settings
6608 settings = Settings()
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00006609
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006610 if metrics.collector.config.should_collect_metrics:
6611 try:
6612 # GetViewVCUrl ultimately calls logging method.
6613 project_url = settings.GetViewVCUrl().strip('/+')
6614 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
6615 metrics.collector.add('project_urls', [project_url])
6616 except subprocess2.CalledProcessError:
6617 # Occurs when command is not executed in a git repository
6618 # We should not fail here. If the command needs to be
6619 # executed in a repo, it will be raised later.
6620 pass
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006621
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006622 def _parse_args(self, args=None):
6623 # Create an optparse.Values object that will store only the actual
6624 # passed options, without the defaults.
6625 actual_options = optparse.Values()
6626 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
6627 # Create an optparse.Values object with the default options.
6628 options = optparse.Values(self.get_default_values().__dict__)
6629 # Update it with the options passed by the user.
6630 options._update_careful(actual_options.__dict__)
6631 # Store the options passed by the user in an _actual_options attribute.
6632 # We store only the keys, and not the values, since the values can
6633 # contain arbitrary information, which might be PII.
6634 metrics.collector.add('arguments', list(actual_options.__dict__.keys()))
Edward Lemur83bd7f42018-10-10 00:14:21 +00006635
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006636 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
6637 logging.basicConfig(
6638 level=levels[min(options.verbose,
6639 len(levels) - 1)],
6640 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
6641 '%(filename)s] %(message)s')
6642
6643 return options, args
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006644
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006645
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006646def main(argv):
Gavin Mak7f5b53f2023-09-07 18:13:01 +00006647 if sys.version_info[0] < 3:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006648 print('\nYour Python version %s is unsupported, please upgrade.\n' %
6649 (sys.version.split(' ', 1)[0], ),
6650 file=sys.stderr)
6651 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006652
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006653 colorize_CMDstatus_doc()
6654 dispatcher = subcommand.CommandDispatcher(__name__)
6655 try:
6656 return dispatcher.execute(OptionParser(), argv)
6657 except auth.LoginRequiredError as e:
6658 DieWithError(str(e))
6659 except urllib.error.HTTPError as e:
6660 if e.code != 500:
6661 raise
6662 DieWithError((
6663 'App Engine is misbehaving and returned HTTP %d, again. Keep faith '
6664 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
6665 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006666
6667
6668if __name__ == '__main__':
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006669 # These affect sys.stdout, so do it outside of main() to simplify mocks in
6670 # the unit tests.
6671 fix_encoding.fix_encoding()
6672 setup_color.init()
6673 with metrics.collector.print_notice_and_exit():
6674 sys.exit(main(sys.argv[1:]))