blob: 20645c6228e5435b41cad89e919d7ef731e61eed [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
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000044import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000045import gerrit_util
iannucci@chromium.org9e849272014-04-04 00:31:55 +000046import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000047import git_footers
Edward Lemur85153282020-02-14 22:06:29 +000048import git_new_branch
Edward Lemur5ba1e9c2018-07-23 18:19:02 +000049import metrics
Edward Lesmes93277a72018-10-18 22:04:26 +000050import metrics_utils
Edward Lesmeseeca9c62020-11-20 00:00:17 +000051import owners_client
iannucci@chromium.org9e849272014-04-04 00:31:55 +000052import owners_finder
Lei Zhangb8c62cf2020-07-15 20:09:37 +000053import presubmit_canned_checks
Josip Sokcevic7958e302023-03-01 23:02:21 +000054import presubmit_support
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +000055import rustfmt
Josip Sokcevic7958e302023-03-01 23:02:21 +000056import scm
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +000057import setup_color
Francois Dorayd42c6812017-05-30 15:10:20 -040058import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000059import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060import subprocess2
Olivier Robin0a6b5442022-04-07 07:25:04 +000061import swift_format
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062import watchlists
63
Edward Lemur79d4f992019-11-11 23:49:02 +000064
tandrii7400cf02016-06-21 08:48:07 -070065__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000066
Mike Frysinger124bb8e2023-09-06 05:48:55 +000067# TODO: Should fix these warnings.
68# pylint: disable=line-too-long
69
Edward Lemur0f58ae42019-04-30 17:24:12 +000070# Traces for git push will be stored in a traces directory inside the
71# depot_tools checkout.
72DEPOT_TOOLS = os.path.dirname(os.path.abspath(__file__))
73TRACES_DIR = os.path.join(DEPOT_TOOLS, 'traces')
Edward Lemur227d5102020-02-25 23:45:35 +000074PRESUBMIT_SUPPORT = os.path.join(DEPOT_TOOLS, 'presubmit_support.py')
Edward Lemur0f58ae42019-04-30 17:24:12 +000075
76# When collecting traces, Git hashes will be reduced to 6 characters to reduce
77# the size after compression.
78GIT_HASH_RE = re.compile(r'\b([a-f0-9]{6})[a-f0-9]{34}\b', flags=re.I)
79# Used to redact the cookies from the gitcookies file.
80GITCOOKIES_REDACT_RE = re.compile(r'1/.*')
81
Edward Lemurd4d1ba42019-09-20 21:46:37 +000082MAX_ATTEMPTS = 3
83
Edward Lemur1b52d872019-05-09 21:12:12 +000084# The maximum number of traces we will keep. Multiplied by 3 since we store
85# 3 files per trace.
86MAX_TRACES = 3 * 10
Edward Lemur5737f022019-05-17 01:24:00 +000087# Message to be displayed to the user to inform where to find the traces for a
88# git-cl upload execution.
Edward Lemur0f58ae42019-04-30 17:24:12 +000089TRACES_MESSAGE = (
Mike Frysinger124bb8e2023-09-06 05:48:55 +000090 '\n'
91 'The traces of this git-cl execution have been recorded at:\n'
92 ' %(trace_name)s-traces.zip\n'
93 'Copies of your gitcookies file and git config have been recorded at:\n'
94 ' %(trace_name)s-git-info.zip\n')
Edward Lemur1b52d872019-05-09 21:12:12 +000095# Format of the message to be stored as part of the traces to give developers a
96# better context when they go through traces.
Mike Frysinger124bb8e2023-09-06 05:48:55 +000097TRACES_README_FORMAT = ('Date: %(now)s\n'
98 '\n'
99 'Change: https://%(gerrit_host)s/q/%(change_id)s\n'
100 'Title: %(title)s\n'
101 '\n'
102 '%(description)s\n'
103 '\n'
104 'Execution time: %(execution_time)s\n'
105 'Exit code: %(exit_code)s\n') + TRACES_MESSAGE
Edward Lemur0f58ae42019-04-30 17:24:12 +0000106
Aaron Gable1bc7bfe2016-12-19 10:08:14 -0800107POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
Henrique Ferreiroff249622019-11-28 23:19:29 +0000108DESCRIPTION_BACKUP_FILE = '.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +0000109REFS_THAT_ALIAS_TO_OTHER_REFS = {
Josip Sokcevic7e133ff2021-07-13 17:44:53 +0000110 'refs/remotes/origin/lkgr': 'refs/remotes/origin/main',
111 'refs/remotes/origin/lkcr': 'refs/remotes/origin/main',
rmistry@google.comc68112d2015-03-03 12:48:06 +0000112}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000113
Josip Sokcevicc39ab992020-09-24 20:09:15 +0000114DEFAULT_OLD_BRANCH = 'refs/remotes/origin/master'
115DEFAULT_NEW_BRANCH = 'refs/remotes/origin/main'
116
Joanna Wanga8db0cb2023-01-24 15:43:17 +0000117DEFAULT_BUILDBUCKET_HOST = 'cr-buildbucket.appspot.com'
118
thestig@chromium.org44202a22014-03-11 19:22:18 +0000119# Valid extensions for files we want to lint.
120DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
121DEFAULT_LINT_IGNORE_REGEX = r"$^"
122
Aiden Bennerc08566e2018-10-03 17:52:42 +0000123# File name for yapf style config files.
124YAPF_CONFIG_FILENAME = '.style.yapf'
125
Edward Lesmes50da7702020-03-30 19:23:43 +0000126# The issue, patchset and codereview server are stored on git config for each
127# branch under branch.<branch-name>.<config-key>.
128ISSUE_CONFIG_KEY = 'gerritissue'
129PATCHSET_CONFIG_KEY = 'gerritpatchset'
130CODEREVIEW_SERVER_CONFIG_KEY = 'gerritserver'
Gavin Makbe2e9262022-11-08 23:41:55 +0000131# When using squash workflow, _CMDUploadChange doesn't simply push the commit(s)
132# you make to Gerrit. Instead, it creates a new commit object that contains all
133# changes you've made, diffed against a parent/merge base.
134# This is the hash of the new squashed commit and you can find this on Gerrit.
135GERRIT_SQUASH_HASH_CONFIG_KEY = 'gerritsquashhash'
136# This is the latest uploaded local commit hash.
137LAST_UPLOAD_HASH_CONFIG_KEY = 'last-upload-hash'
Edward Lesmes50da7702020-03-30 19:23:43 +0000138
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000139# Shortcut since it quickly becomes repetitive.
maruel@chromium.org2e23ce32013-05-07 12:42:28 +0000140Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +0000141
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000142# Initialized in main()
143settings = None
144
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100145# Used by tests/git_cl_test.py to add extra logging.
146# Inside the weirdly failing test, add this:
147# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700148# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100149_IS_BEING_TESTED = False
150
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000151_GOOGLESOURCE = 'googlesource.com'
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000152
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +0000153_KNOWN_GERRIT_TO_SHORT_URLS = {
154 'https://chrome-internal-review.googlesource.com': 'https://crrev.com/i',
155 'https://chromium-review.googlesource.com': 'https://crrev.com/c',
156}
Andrii Shyshkalov8aebb602020-04-16 22:10:27 +0000157assert len(_KNOWN_GERRIT_TO_SHORT_URLS) == len(
158 set(_KNOWN_GERRIT_TO_SHORT_URLS.values())), 'must have unique values'
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +0000159
Joanna Wang18de1f62023-01-21 01:24:24 +0000160# Maximum number of branches in a stack that can be traversed and uploaded
161# at once. Picked arbitrarily.
162_MAX_STACKED_BRANCHES_UPLOAD = 20
163
Joanna Wang892f2ce2023-03-14 21:39:47 +0000164# Environment variable to indicate if user is participating in the stcked
165# changes dogfood.
166DOGFOOD_STACKED_CHANGES_VAR = 'DOGFOOD_STACKED_CHANGES'
167
168
Josip Sokcevicf736cab2020-10-20 23:41:38 +0000169class GitPushError(Exception):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000170 pass
Josip Sokcevicf736cab2020-10-20 23:41:38 +0000171
172
Daniel Chengabf48472023-08-30 15:45:13 +0000173def DieWithError(message, change_desc=None) -> NoReturn:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000174 if change_desc:
175 SaveDescriptionBackup(change_desc)
176 print('\n ** Content of CL description **\n' + '=' * 72 + '\n' +
177 change_desc.description + '\n' + '=' * 72 + '\n')
Christopher Lamf732cd52017-01-24 12:40:11 +1100178
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000179 print(message, file=sys.stderr)
180 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000181
182
Christopher Lamf732cd52017-01-24 12:40:11 +1100183def SaveDescriptionBackup(change_desc):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000184 backup_path = os.path.join(DEPOT_TOOLS, DESCRIPTION_BACKUP_FILE)
185 print('\nsaving CL description to %s\n' % backup_path)
186 with open(backup_path, 'wb') as backup_file:
187 backup_file.write(change_desc.description.encode('utf-8'))
Christopher Lamf732cd52017-01-24 12:40:11 +1100188
189
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000190def GetNoGitPagerEnv():
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000191 env = os.environ.copy()
192 # 'cat' is a magical git string that disables pagers on all platforms.
193 env['GIT_PAGER'] = 'cat'
194 return env
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000195
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000196
bsep@chromium.org627d9002016-04-29 00:00:52 +0000197def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000198 try:
199 stdout = subprocess2.check_output(args, shell=shell, **kwargs)
200 return stdout.decode('utf-8', 'replace')
201 except subprocess2.CalledProcessError as e:
202 logging.debug('Failed running %s', args)
203 if not error_ok:
204 message = error_message or e.stdout.decode('utf-8', 'replace') or ''
205 DieWithError('Command "%s" failed.\n%s' % (' '.join(args), message))
206 out = e.stdout.decode('utf-8', 'replace')
207 if e.stderr:
208 out += e.stderr.decode('utf-8', 'replace')
209 return out
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000210
211
212def RunGit(args, **kwargs):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000213 """Returns stdout."""
214 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000215
216
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000217def RunGitWithCode(args, suppress_stderr=False):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000218 """Returns return code and stdout."""
219 if suppress_stderr:
220 stderr = subprocess2.DEVNULL
221 else:
222 stderr = sys.stderr
223 try:
224 (out, _), code = subprocess2.communicate(['git'] + args,
225 env=GetNoGitPagerEnv(),
226 stdout=subprocess2.PIPE,
227 stderr=stderr)
228 return code, out.decode('utf-8', 'replace')
229 except subprocess2.CalledProcessError as e:
230 logging.debug('Failed running %s', ['git'] + args)
231 return e.returncode, e.stdout.decode('utf-8', 'replace')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000232
233
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000234def RunGitSilent(args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000235 """Returns stdout, suppresses stderr and ignores the return code."""
236 return RunGitWithCode(args, suppress_stderr=True)[1]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000237
238
tandrii2a16b952016-10-19 07:09:44 -0700239def time_sleep(seconds):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000240 # Use this so that it can be mocked in tests without interfering with python
241 # system machinery.
242 return time.sleep(seconds)
tandrii2a16b952016-10-19 07:09:44 -0700243
244
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000245def time_time():
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000246 # Use this so that it can be mocked in tests without interfering with python
247 # system machinery.
248 return time.time()
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000249
250
Edward Lemur1b52d872019-05-09 21:12:12 +0000251def datetime_now():
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000252 # Use this so that it can be mocked in tests without interfering with python
253 # system machinery.
254 return datetime.datetime.now()
Edward Lemur1b52d872019-05-09 21:12:12 +0000255
256
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100257def confirm_or_exit(prefix='', action='confirm'):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000258 """Asks user to press enter to continue or press Ctrl+C to abort."""
259 if not prefix or prefix.endswith('\n'):
260 mid = 'Press'
261 elif prefix.endswith('.') or prefix.endswith('?'):
262 mid = ' Press'
263 elif prefix.endswith(' '):
264 mid = 'press'
265 else:
266 mid = ' press'
267 gclient_utils.AskForData('%s%s Enter to %s, or Ctrl+C to abort' %
268 (prefix, mid, action))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100269
270
271def ask_for_explicit_yes(prompt):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000272 """Returns whether user typed 'y' or 'yes' to confirm the given prompt."""
273 result = gclient_utils.AskForData(prompt + ' [Yes/No]: ').lower()
274 while True:
275 if 'yes'.startswith(result):
276 return True
277 if 'no'.startswith(result):
278 return False
279 result = gclient_utils.AskForData('Please, type yes or no: ').lower()
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100280
281
machenbach@chromium.org45453142015-09-15 08:45:22 +0000282def _get_properties_from_options(options):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000283 prop_list = getattr(options, 'properties', [])
284 properties = dict(x.split('=', 1) for x in prop_list)
285 for key, val in properties.items():
286 try:
287 properties[key] = json.loads(val)
288 except ValueError:
289 pass # If a value couldn't be evaluated, treat it as a string.
290 return properties
machenbach@chromium.org45453142015-09-15 08:45:22 +0000291
292
Edward Lemur4c707a22019-09-24 21:13:43 +0000293def _call_buildbucket(http, buildbucket_host, method, request):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000294 """Calls a buildbucket v2 method and returns the parsed json response."""
295 headers = {
296 'Accept': 'application/json',
297 'Content-Type': 'application/json',
298 }
299 request = json.dumps(request)
300 url = 'https://%s/prpc/buildbucket.v2.Builds/%s' % (buildbucket_host,
301 method)
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000302
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000303 logging.info('POST %s with %s' % (url, request))
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000304
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000305 attempts = 1
306 time_to_sleep = 1
307 while True:
308 response, content = http.request(url,
309 'POST',
310 body=request,
311 headers=headers)
312 if response.status == 200:
313 return json.loads(content[4:])
314 if attempts >= MAX_ATTEMPTS or 400 <= response.status < 500:
315 msg = '%s error when calling POST %s with %s: %s' % (
316 response.status, url, request, content)
317 raise BuildbucketResponseException(msg)
318 logging.debug('%s error when calling POST %s with %s. '
319 'Sleeping for %d seconds and retrying...' %
320 (response.status, url, request, time_to_sleep))
321 time.sleep(time_to_sleep)
322 time_to_sleep *= 2
323 attempts += 1
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000324
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000325 assert False, 'unreachable'
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000326
327
Edward Lemur6215c792019-10-03 21:59:05 +0000328def _parse_bucket(raw_bucket):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000329 legacy = True
330 project = bucket = None
331 if '/' in raw_bucket:
332 legacy = False
333 project, bucket = raw_bucket.split('/', 1)
334 # Assume luci.<project>.<bucket>.
335 elif raw_bucket.startswith('luci.'):
336 project, bucket = raw_bucket[len('luci.'):].split('.', 1)
337 # Otherwise, assume prefix is also the project name.
338 elif '.' in raw_bucket:
339 project = raw_bucket.split('.')[0]
340 bucket = raw_bucket
341 # Legacy buckets.
342 if legacy and project and bucket:
343 print('WARNING Please use %s/%s to specify the bucket.' %
344 (project, bucket))
345 return project, bucket
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000346
347
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000348def _canonical_git_googlesource_host(host):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000349 """Normalizes Gerrit hosts (with '-review') to Git host."""
350 assert host.endswith(_GOOGLESOURCE)
351 # Prefix doesn't include '.' at the end.
352 prefix = host[:-(1 + len(_GOOGLESOURCE))]
353 if prefix.endswith('-review'):
354 prefix = prefix[:-len('-review')]
355 return prefix + '.' + _GOOGLESOURCE
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000356
357
358def _canonical_gerrit_googlesource_host(host):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000359 git_host = _canonical_git_googlesource_host(host)
360 prefix = git_host.split('.', 1)[0]
361 return prefix + '-review.' + _GOOGLESOURCE
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000362
363
364def _get_counterpart_host(host):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000365 assert host.endswith(_GOOGLESOURCE)
366 git = _canonical_git_googlesource_host(host)
367 gerrit = _canonical_gerrit_googlesource_host(git)
368 return git if gerrit == host else gerrit
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000369
370
Quinten Yearsley777660f2020-03-04 23:37:06 +0000371def _trigger_tryjobs(changelist, jobs, options, patchset):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000372 """Sends a request to Buildbucket to trigger tryjobs for a changelist.
qyearsley1fdfcb62016-10-24 13:22:03 -0700373
374 Args:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000375 changelist: Changelist that the tryjobs are associated with.
Edward Lemur45768512020-03-02 19:03:14 +0000376 jobs: A list of (project, bucket, builder).
qyearsley1fdfcb62016-10-24 13:22:03 -0700377 options: Command-line options.
378 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000379 print('Scheduling jobs on:')
380 for project, bucket, builder in jobs:
381 print(' %s/%s: %s' % (project, bucket, builder))
382 print('To see results here, run: git cl try-results')
383 print('To see results in browser, run: git cl web')
tandriide281ae2016-10-12 06:02:30 -0700384
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000385 requests = _make_tryjob_schedule_requests(changelist, jobs, options,
386 patchset)
387 if not requests:
388 return
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000389
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000390 http = auth.Authenticator().authorize(httplib2.Http())
391 http.force_exception_to_status_code = True
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000392
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000393 batch_request = {'requests': requests}
394 batch_response = _call_buildbucket(http, DEFAULT_BUILDBUCKET_HOST, 'Batch',
395 batch_request)
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000396
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000397 errors = [
398 ' ' + response['error']['message']
399 for response in batch_response.get('responses', [])
400 if 'error' in response
401 ]
402 if errors:
403 raise BuildbucketResponseException(
404 'Failed to schedule builds for some bots:\n%s' % '\n'.join(errors))
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000405
406
Quinten Yearsley777660f2020-03-04 23:37:06 +0000407def _make_tryjob_schedule_requests(changelist, jobs, options, patchset):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000408 """Constructs requests for Buildbucket to trigger tryjobs."""
409 gerrit_changes = [changelist.GetGerritChange(patchset)]
410 shared_properties = {
411 'category': options.ensure_value('category', 'git_cl_try')
412 }
413 if options.ensure_value('clobber', False):
414 shared_properties['clobber'] = True
415 shared_properties.update(_get_properties_from_options(options) or {})
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000416
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000417 shared_tags = [{'key': 'user_agent', 'value': 'git_cl_try'}]
418 if options.ensure_value('retry_failed', False):
419 shared_tags.append({'key': 'retry_failed', 'value': '1'})
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000420
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000421 requests = []
422 for (project, bucket, builder) in jobs:
423 properties = shared_properties.copy()
424 if 'presubmit' in builder.lower():
425 properties['dry_run'] = 'true'
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000426
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000427 requests.append({
428 'scheduleBuild': {
429 'requestId': str(uuid.uuid4()),
430 'builder': {
431 'project': getattr(options, 'project', None) or project,
432 'bucket': bucket,
433 'builder': builder,
434 },
435 'gerritChanges': gerrit_changes,
436 'properties': properties,
437 'tags': [
438 {
439 'key': 'builder',
440 'value': builder
441 },
442 ] + shared_tags,
443 }
444 })
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000445
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000446 if options.ensure_value('revision', None):
447 remote, remote_branch = changelist.GetRemoteBranch()
448 requests[-1]['scheduleBuild']['gitilesCommit'] = {
449 'host':
450 _canonical_git_googlesource_host(gerrit_changes[0]['host']),
451 'project': gerrit_changes[0]['project'],
452 'id': options.revision,
453 'ref': GetTargetRef(remote, remote_branch, None)
454 }
Anthony Polito1a5fe232020-01-24 23:17:52 +0000455
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000456 return requests
kjellander@chromium.org44424542015-06-02 18:35:29 +0000457
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000458
Quinten Yearsley777660f2020-03-04 23:37:06 +0000459def _fetch_tryjobs(changelist, buildbucket_host, patchset=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000460 """Fetches tryjobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000461
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000462 Returns list of buildbucket.v2.Build with the try jobs for the changelist.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000463 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000464 fields = ['id', 'builder', 'status', 'createTime', 'tags']
465 request = {
466 'predicate': {
467 'gerritChanges': [changelist.GetGerritChange(patchset)],
468 },
469 'fields': ','.join('builds.*.' + field for field in fields),
470 }
tandrii221ab252016-10-06 08:12:04 -0700471
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000472 authenticator = auth.Authenticator()
473 if authenticator.has_cached_credentials():
474 http = authenticator.authorize(httplib2.Http())
475 else:
476 print('Warning: Some results might be missing because %s' %
477 # Get the message on how to login.
478 (
479 str(auth.LoginRequiredError()), ))
480 http = httplib2.Http()
481 http.force_exception_to_status_code = True
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000482
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000483 response = _call_buildbucket(http, buildbucket_host, 'SearchBuilds',
484 request)
485 return response.get('builds', [])
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000486
Edward Lemur45768512020-03-02 19:03:14 +0000487
Edward Lemur5b929a42019-10-21 17:57:39 +0000488def _fetch_latest_builds(changelist, buildbucket_host, latest_patchset=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000489 """Fetches builds from the latest patchset that has builds (within
Quinten Yearsley983111f2019-09-26 17:18:48 +0000490 the last few patchsets).
491
492 Args:
Quinten Yearsley983111f2019-09-26 17:18:48 +0000493 changelist (Changelist): The CL to fetch builds for
494 buildbucket_host (str): Buildbucket host, e.g. "cr-buildbucket.appspot.com"
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +0000495 lastest_patchset(int|NoneType): the patchset to start fetching builds from.
496 If None (default), starts with the latest available patchset.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000497 Returns:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000498 A tuple (builds, patchset) where builds is a list of buildbucket.v2.Build,
499 and patchset is the patchset number where those builds came from.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000500 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000501 assert buildbucket_host
502 assert changelist.GetIssue(), 'CL must be uploaded first'
503 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
504 if latest_patchset is None:
505 assert changelist.GetMostRecentPatchset()
506 ps = changelist.GetMostRecentPatchset()
507 else:
508 assert latest_patchset > 0, latest_patchset
509 ps = latest_patchset
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +0000510
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000511 min_ps = max(1, ps - 5)
512 while ps >= min_ps:
513 builds = _fetch_tryjobs(changelist, buildbucket_host, patchset=ps)
514 if len(builds):
515 return builds, ps
516 ps -= 1
517 return [], 0
Quinten Yearsley983111f2019-09-26 17:18:48 +0000518
519
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000520def _filter_failed_for_retry(all_builds):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000521 """Returns a list of buckets/builders that are worth retrying.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000522
523 Args:
Quinten Yearsley777660f2020-03-04 23:37:06 +0000524 all_builds (list): Builds, in the format returned by _fetch_tryjobs,
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000525 i.e. a list of buildbucket.v2.Builds which includes status and builder
526 info.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000527
528 Returns:
Edward Lemur45768512020-03-02 19:03:14 +0000529 A dict {(proj, bucket): [builders]}. This is the same format accepted by
Quinten Yearsley777660f2020-03-04 23:37:06 +0000530 _trigger_tryjobs.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000531 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000532 grouped = {}
533 for build in all_builds:
534 builder = build['builder']
535 key = (builder['project'], builder['bucket'], builder['builder'])
536 grouped.setdefault(key, []).append(build)
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000537
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000538 jobs = []
539 for (project, bucket, builder), builds in grouped.items():
540 if 'triggered' in builder:
541 print(
542 'WARNING: Not scheduling %s. Triggered bots require an initial job '
543 'from a parent. Please schedule a manual job for the parent '
544 'instead.')
545 continue
546 if any(b['status'] in ('STARTED', 'SCHEDULED') for b in builds):
547 # Don't retry if any are running.
548 continue
549 # If builder had several builds, retry only if the last one failed.
550 # This is a bit different from CQ, which would re-use *any* SUCCESS-full
551 # build, but in case of retrying failed jobs retrying a flaky one makes
552 # sense.
553 builds = sorted(builds, key=lambda b: b['createTime'])
554 if builds[-1]['status'] not in ('FAILURE', 'INFRA_FAILURE'):
555 continue
556 # Don't retry experimental build previously triggered by CQ.
557 if any(t['key'] == 'cq_experimental' and t['value'] == 'true'
558 for t in builds[-1]['tags']):
559 continue
560 jobs.append((project, bucket, builder))
Edward Lemur45768512020-03-02 19:03:14 +0000561
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000562 # Sort the jobs to make testing easier.
563 return sorted(jobs)
Quinten Yearsley983111f2019-09-26 17:18:48 +0000564
565
Quinten Yearsley777660f2020-03-04 23:37:06 +0000566def _print_tryjobs(options, builds):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000567 """Prints nicely result of _fetch_tryjobs."""
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000568 if not builds:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000569 print('No tryjobs scheduled.')
570 return
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000571
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000572 longest_builder = max(len(b['builder']['builder']) for b in builds)
573 name_fmt = '{builder:<%d}' % longest_builder
574 if options.print_master:
575 longest_bucket = max(len(b['builder']['bucket']) for b in builds)
576 name_fmt = ('{bucket:>%d} ' % longest_bucket) + name_fmt
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000577
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000578 builds_by_status = {}
579 for b in builds:
580 builds_by_status.setdefault(b['status'], []).append({
581 'id':
582 b['id'],
583 'name':
584 name_fmt.format(builder=b['builder']['builder'],
585 bucket=b['builder']['bucket']),
586 })
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000587
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000588 sort_key = lambda b: (b['name'], b['id'])
589
590 def print_builds(title, builds, fmt=None, color=None):
591 """Pop matching builds from `builds` dict and print them."""
592 if not builds:
593 return
594
595 fmt = fmt or '{name} https://ci.chromium.org/b/{id}'
596 if not options.color or color is None:
597 colorize = lambda x: x
598 else:
599 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
600
601 print(colorize(title))
602 for b in sorted(builds, key=sort_key):
603 print(' ', colorize(fmt.format(**b)))
604
605 total = len(builds)
606 print_builds('Successes:',
607 builds_by_status.pop('SUCCESS', []),
608 color=Fore.GREEN)
609 print_builds('Infra Failures:',
610 builds_by_status.pop('INFRA_FAILURE', []),
611 color=Fore.MAGENTA)
612 print_builds('Failures:',
613 builds_by_status.pop('FAILURE', []),
614 color=Fore.RED)
615 print_builds('Canceled:',
616 builds_by_status.pop('CANCELED', []),
617 fmt='{name}',
618 color=Fore.MAGENTA)
619 print_builds('Started:',
620 builds_by_status.pop('STARTED', []),
621 color=Fore.YELLOW)
622 print_builds('Scheduled:',
623 builds_by_status.pop('SCHEDULED', []),
624 fmt='{name} id={id}')
625 # The last section is just in case buildbucket API changes OR there is a
626 # bug.
627 print_builds('Other:',
628 sum(builds_by_status.values(), []),
629 fmt='{name} id={id}')
630 print('Total: %d tryjobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000631
632
Aiden Bennerc08566e2018-10-03 17:52:42 +0000633def _ComputeDiffLineRanges(files, upstream_commit):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000634 """Gets the changed line ranges for each file since upstream_commit.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000635
636 Parses a git diff on provided files and returns a dict that maps a file name
637 to an ordered list of range tuples in the form (start_line, count).
638 Ranges are in the same format as a git diff.
639 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000640 # If files is empty then diff_output will be a full diff.
641 if len(files) == 0:
642 return {}
Aiden Bennerc08566e2018-10-03 17:52:42 +0000643
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000644 # Take the git diff and find the line ranges where there are changes.
645 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
646 diff_output = RunGit(diff_cmd)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000647
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000648 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
649 # 2 capture groups
650 # 0 == fname of diff file
651 # 1 == 'diff_start,diff_count' or 'diff_start'
652 # will match each of
653 # diff --git a/foo.foo b/foo.py
654 # @@ -12,2 +14,3 @@
655 # @@ -12,2 +17 @@
656 # running re.findall on the above string with pattern will give
657 # [('foo.py', ''), ('', '14,3'), ('', '17')]
Aiden Bennerc08566e2018-10-03 17:52:42 +0000658
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000659 curr_file = None
660 line_diffs = {}
661 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
662 if match[0] != '':
663 # Will match the second filename in diff --git a/a.py b/b.py.
664 curr_file = match[0]
665 line_diffs[curr_file] = []
666 else:
667 # Matches +14,3
668 if ',' in match[1]:
669 diff_start, diff_count = match[1].split(',')
670 else:
671 # Single line changes are of the form +12 instead of +12,1.
672 diff_start = match[1]
673 diff_count = 1
Aiden Bennerc08566e2018-10-03 17:52:42 +0000674
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000675 diff_start = int(diff_start)
676 diff_count = int(diff_count)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000677
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000678 # If diff_count == 0 this is a removal we can ignore.
679 line_diffs[curr_file].append((diff_start, diff_count))
Aiden Bennerc08566e2018-10-03 17:52:42 +0000680
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000681 return line_diffs
Aiden Bennerc08566e2018-10-03 17:52:42 +0000682
683
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000684def _FindYapfConfigFile(fpath, yapf_config_cache, top_dir=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000685 """Checks if a yapf file is in any parent directory of fpath until top_dir.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000686
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000687 Recursively checks parent directories to find yapf file and if no yapf file
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000688 is found returns None. Uses yapf_config_cache as a cache for previously found
689 configs.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000690 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000691 fpath = os.path.abspath(fpath)
692 # Return result if we've already computed it.
693 if fpath in yapf_config_cache:
694 return yapf_config_cache[fpath]
Aiden Bennerc08566e2018-10-03 17:52:42 +0000695
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000696 parent_dir = os.path.dirname(fpath)
697 if os.path.isfile(fpath):
698 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000699 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000700 # Otherwise fpath is a directory
701 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
702 if os.path.isfile(yapf_file):
703 ret = yapf_file
704 elif fpath in (top_dir, parent_dir):
705 # If we're at the top level directory, or if we're at root
706 # there is no provided style.
707 ret = None
708 else:
709 # Otherwise recurse on the current directory.
710 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
711 yapf_config_cache[fpath] = ret
712 return ret
Aiden Bennerc08566e2018-10-03 17:52:42 +0000713
714
Brian Sheedyb4307d52019-12-02 19:18:17 +0000715def _GetYapfIgnorePatterns(top_dir):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000716 """Returns all patterns in the .yapfignore file.
Brian Sheedy59b06a82019-10-14 17:03:29 +0000717
718 yapf is supposed to handle the ignoring of files listed in .yapfignore itself,
719 but this functionality appears to break when explicitly passing files to
720 yapf for formatting. According to
Josip Sokcevicc39ab992020-09-24 20:09:15 +0000721 https://github.com/google/yapf/blob/HEAD/README.rst#excluding-files-from-formatting-yapfignore,
Brian Sheedy59b06a82019-10-14 17:03:29 +0000722 the .yapfignore file should be in the directory that yapf is invoked from,
723 which we assume to be the top level directory in this case.
724
725 Args:
726 top_dir: The top level directory for the repository being formatted.
727
728 Returns:
Brian Sheedyb4307d52019-12-02 19:18:17 +0000729 A set of all fnmatch patterns to be ignored.
Brian Sheedy59b06a82019-10-14 17:03:29 +0000730 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000731 yapfignore_file = os.path.join(top_dir, '.yapfignore')
732 ignore_patterns = set()
733 if not os.path.exists(yapfignore_file):
734 return ignore_patterns
Brian Sheedy59b06a82019-10-14 17:03:29 +0000735
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000736 for line in gclient_utils.FileRead(yapfignore_file).split('\n'):
737 stripped_line = line.strip()
738 # Comments and blank lines should be ignored.
739 if stripped_line.startswith('#') or stripped_line == '':
740 continue
741 ignore_patterns.add(stripped_line)
742 return ignore_patterns
Brian Sheedyb4307d52019-12-02 19:18:17 +0000743
744
745def _FilterYapfIgnoredFiles(filepaths, patterns):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000746 """Filters out any filepaths that match any of the given patterns.
Brian Sheedyb4307d52019-12-02 19:18:17 +0000747
748 Args:
749 filepaths: An iterable of strings containing filepaths to filter.
750 patterns: An iterable of strings containing fnmatch patterns to filter on.
751
752 Returns:
753 A list of strings containing all the elements of |filepaths| that did not
754 match any of the patterns in |patterns|.
755 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000756 # Not inlined so that tests can use the same implementation.
757 return [
758 f for f in filepaths
759 if not any(fnmatch.fnmatch(f, p) for p in patterns)
760 ]
Brian Sheedy59b06a82019-10-14 17:03:29 +0000761
762
Daniel Cheng66d0f152023-08-29 23:21:58 +0000763def _GetCommitCountSummary(begin_commit: str,
764 end_commit: str = "HEAD") -> Optional[str]:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000765 """Generate a summary of the number of commits in (begin_commit, end_commit).
Daniel Cheng66d0f152023-08-29 23:21:58 +0000766
767 Returns a string containing the summary, or None if the range is empty.
768 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000769 count = int(
770 RunGitSilent(['rev-list', '--count', f'{begin_commit}..{end_commit}']))
Daniel Cheng66d0f152023-08-29 23:21:58 +0000771
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000772 if not count:
773 return None
Daniel Cheng66d0f152023-08-29 23:21:58 +0000774
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000775 return f'{count} commit{"s"[:count!=1]}'
Daniel Cheng66d0f152023-08-29 23:21:58 +0000776
777
Aaron Gable13101a62018-02-09 13:20:41 -0800778def print_stats(args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000779 """Prints statistics about the change to the user."""
780 # --no-ext-diff is broken in some versions of Git, so try to work around
781 # this by overriding the environment (but there is still a problem if the
782 # git config key "diff.external" is used).
783 env = GetNoGitPagerEnv()
784 if 'GIT_EXTERNAL_DIFF' in env:
785 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000786
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000787 return subprocess2.call(
788 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
789 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000790
791
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000792class BuildbucketResponseException(Exception):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000793 pass
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000794
795
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000796class Settings(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000797 def __init__(self):
798 self.cc = None
799 self.root = None
800 self.tree_status_url = None
801 self.viewvc_url = None
802 self.updated = False
803 self.is_gerrit = None
804 self.squash_gerrit_uploads = None
805 self.gerrit_skip_ensure_authenticated = None
806 self.git_editor = None
807 self.format_full_by_default = None
808 self.is_status_commit_order_by_date = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000809
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000810 def _LazyUpdateIfNeeded(self):
811 """Updates the settings from a codereview.settings file, if available."""
812 if self.updated:
813 return
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000814
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000815 # The only value that actually changes the behavior is
816 # autoupdate = "false". Everything else means "true".
817 autoupdate = (scm.GIT.GetConfig(self.GetRoot(), 'rietveld.autoupdate',
818 '').lower())
Edward Lemur26964072020-02-19 19:18:51 +0000819
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000820 cr_settings_file = FindCodereviewSettingsFile()
821 if autoupdate != 'false' and cr_settings_file:
822 LoadCodereviewSettingsFromFile(cr_settings_file)
823 cr_settings_file.close()
Edward Lemur26964072020-02-19 19:18:51 +0000824
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000825 self.updated = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000826
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000827 @staticmethod
828 def GetRelativeRoot():
829 return scm.GIT.GetCheckoutRoot('.')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000830
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000831 def GetRoot(self):
832 if self.root is None:
833 self.root = os.path.abspath(self.GetRelativeRoot())
834 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000835
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000836 def GetTreeStatusUrl(self, error_ok=False):
837 if not self.tree_status_url:
838 self.tree_status_url = self._GetConfig('rietveld.tree-status-url')
839 if self.tree_status_url is None and not error_ok:
840 DieWithError(
841 'You must configure your tree status URL by running '
842 '"git cl config".')
843 return self.tree_status_url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000844
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000845 def GetViewVCUrl(self):
846 if not self.viewvc_url:
847 self.viewvc_url = self._GetConfig('rietveld.viewvc-url')
848 return self.viewvc_url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000849
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000850 def GetBugPrefix(self):
851 return self._GetConfig('rietveld.bug-prefix')
rmistry@google.com78948ed2015-07-08 23:09:57 +0000852
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000853 def GetRunPostUploadHook(self):
854 run_post_upload_hook = self._GetConfig('rietveld.run-post-upload-hook')
855 return run_post_upload_hook == "True"
rmistry@google.com5626a922015-02-26 14:03:30 +0000856
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000857 def GetDefaultCCList(self):
858 return self._GetConfig('rietveld.cc')
Joanna Wangc8f23e22023-01-19 21:18:10 +0000859
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000860 def GetSquashGerritUploads(self):
861 """Returns True if uploads to Gerrit should be squashed by default."""
862 if self.squash_gerrit_uploads is None:
863 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
864 if self.squash_gerrit_uploads is None:
865 # Default is squash now (http://crbug.com/611892#c23).
866 self.squash_gerrit_uploads = self._GetConfig(
867 'gerrit.squash-uploads').lower() != 'false'
868 return self.squash_gerrit_uploads
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000869
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000870 def GetSquashGerritUploadsOverride(self):
871 """Return True or False if codereview.settings should be overridden.
Edward Lesmes4de54132020-05-05 19:41:33 +0000872
873 Returns None if no override has been defined.
874 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000875 # See also http://crbug.com/611892#c23
876 result = self._GetConfig('gerrit.override-squash-uploads').lower()
877 if result == 'true':
878 return True
879 if result == 'false':
880 return False
881 return None
Edward Lesmes4de54132020-05-05 19:41:33 +0000882
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000883 def GetIsGerrit(self):
884 """Return True if gerrit.host is set."""
885 if self.is_gerrit is None:
886 self.is_gerrit = bool(self._GetConfig('gerrit.host', False))
887 return self.is_gerrit
Aleksey Khoroshilov35ef5ad2022-06-03 18:29:25 +0000888
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000889 def GetGerritSkipEnsureAuthenticated(self):
890 """Return True if EnsureAuthenticated should not be done for Gerrit
tandrii@chromium.org28253532016-04-14 13:46:56 +0000891 uploads."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000892 if self.gerrit_skip_ensure_authenticated is None:
893 self.gerrit_skip_ensure_authenticated = self._GetConfig(
894 'gerrit.skip-ensure-authenticated').lower() == 'true'
895 return self.gerrit_skip_ensure_authenticated
tandrii@chromium.org28253532016-04-14 13:46:56 +0000896
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000897 def GetGitEditor(self):
898 """Returns the editor specified in the git config, or None if none is."""
899 if self.git_editor is None:
900 # Git requires single quotes for paths with spaces. We need to
901 # replace them with double quotes for Windows to treat such paths as
902 # a single path.
903 self.git_editor = self._GetConfig('core.editor').replace('\'', '"')
904 return self.git_editor or None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000905
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000906 def GetLintRegex(self):
907 return self._GetConfig('rietveld.cpplint-regex', DEFAULT_LINT_REGEX)
thestig@chromium.org44202a22014-03-11 19:22:18 +0000908
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000909 def GetLintIgnoreRegex(self):
910 return self._GetConfig('rietveld.cpplint-ignore-regex',
911 DEFAULT_LINT_IGNORE_REGEX)
thestig@chromium.org44202a22014-03-11 19:22:18 +0000912
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000913 def GetFormatFullByDefault(self):
914 if self.format_full_by_default is None:
915 self._LazyUpdateIfNeeded()
916 result = (RunGit(
917 ['config', '--bool', 'rietveld.format-full-by-default'],
918 error_ok=True).strip())
919 self.format_full_by_default = (result == 'true')
920 return self.format_full_by_default
Jamie Madilldc4d19e2019-10-24 21:50:02 +0000921
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000922 def IsStatusCommitOrderByDate(self):
923 if self.is_status_commit_order_by_date is None:
924 result = (RunGit(['config', '--bool', 'cl.date-order'],
925 error_ok=True).strip())
926 self.is_status_commit_order_by_date = (result == 'true')
927 return self.is_status_commit_order_by_date
Sigurd Schneider1bfda8e2021-06-30 14:46:25 +0000928
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000929 def _GetConfig(self, key, default=''):
930 self._LazyUpdateIfNeeded()
931 return scm.GIT.GetConfig(self.GetRoot(), key, default)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000932
933
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000934class _CQState(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000935 """Enum for states of CL with respect to CQ."""
936 NONE = 'none'
937 DRY_RUN = 'dry_run'
938 COMMIT = 'commit'
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000939
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000940 ALL_STATES = [NONE, DRY_RUN, COMMIT]
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000941
942
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000943class _ParsedIssueNumberArgument(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000944 def __init__(self, issue=None, patchset=None, hostname=None):
945 self.issue = issue
946 self.patchset = patchset
947 self.hostname = hostname
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000948
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000949 @property
950 def valid(self):
951 return self.issue is not None
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000952
953
Edward Lemurf38bc172019-09-03 21:02:13 +0000954def ParseIssueNumberArgument(arg):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000955 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
956 fail_result = _ParsedIssueNumberArgument()
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000957
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000958 if isinstance(arg, int):
959 return _ParsedIssueNumberArgument(issue=arg)
960 if not isinstance(arg, str):
961 return fail_result
Edward Lemur678a6842019-10-03 22:25:05 +0000962
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000963 if arg.isdigit():
964 return _ParsedIssueNumberArgument(issue=int(arg))
Aaron Gableaee6c852017-06-26 12:49:01 -0700965
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000966 url = gclient_utils.UpgradeToHttps(arg)
967 if not url.startswith('http'):
968 return fail_result
969 for gerrit_url, short_url in _KNOWN_GERRIT_TO_SHORT_URLS.items():
970 if url.startswith(short_url):
971 url = gerrit_url + url[len(short_url):]
972 break
Andrii Shyshkalov8aebb602020-04-16 22:10:27 +0000973
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000974 try:
975 parsed_url = urllib.parse.urlparse(url)
976 except ValueError:
977 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +0200978
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000979 # If "https://" was automatically added, fail if `arg` looks unlikely to be
980 # a URL.
981 if not arg.startswith('http') and '.' not in parsed_url.netloc:
982 return fail_result
Alex Turner30ae6372022-01-04 02:32:52 +0000983
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000984 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
985 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
986 # Short urls like https://domain/<issue_number> can be used, but don't allow
987 # specifying the patchset (you'd 404), but we allow that here.
988 if parsed_url.path == '/':
989 part = parsed_url.fragment
990 else:
991 part = parsed_url.path
Edward Lemur678a6842019-10-03 22:25:05 +0000992
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000993 match = re.match(r'(/c(/.*/\+)?)?/(?P<issue>\d+)(/(?P<patchset>\d+)?/?)?$',
994 part)
995 if not match:
996 return fail_result
Edward Lemur678a6842019-10-03 22:25:05 +0000997
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000998 issue = int(match.group('issue'))
999 patchset = match.group('patchset')
1000 return _ParsedIssueNumberArgument(
1001 issue=issue,
1002 patchset=int(patchset) if patchset else None,
1003 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001004
1005
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00001006def _create_description_from_log(args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001007 """Pulls out the commit log to use as a base for the CL description."""
1008 log_args = []
1009 if len(args) == 1 and args[0] == None:
1010 # Handle the case where None is passed as the branch.
1011 return ''
1012 if len(args) == 1 and not args[0].endswith('.'):
1013 log_args = [args[0] + '..']
1014 elif len(args) == 1 and args[0].endswith('...'):
1015 log_args = [args[0][:-1]]
1016 elif len(args) == 2:
1017 log_args = [args[0] + '..' + args[1]]
1018 else:
1019 log_args = args[:] # Hope for the best!
1020 return RunGit(['log', '--pretty=format:%B%n'] + log_args)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00001021
1022
Aaron Gablea45ee112016-11-22 15:14:38 -08001023class GerritChangeNotExists(Exception):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001024 def __init__(self, issue, url):
1025 self.issue = issue
1026 self.url = url
1027 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001028
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001029 def __str__(self):
1030 return 'change %s at %s does not exist or you have no access to it' % (
1031 self.issue, self.url)
tandriic2405f52016-10-10 08:13:15 -07001032
1033
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001034_CommentSummary = collections.namedtuple(
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001035 '_CommentSummary',
1036 [
1037 'date',
1038 'message',
1039 'sender',
1040 'autogenerated',
1041 # TODO(tandrii): these two aren't known in Gerrit.
1042 'approval',
1043 'disapproval'
1044 ])
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001045
Joanna Wang6215dd02023-02-07 15:58:03 +00001046# TODO(b/265929888): Change `parent` to `pushed_commit_base`.
Joanna Wange8523912023-01-21 02:05:40 +00001047_NewUpload = collections.namedtuple('NewUpload', [
Joanna Wang40497912023-01-24 21:18:16 +00001048 'reviewers', 'ccs', 'commit_to_push', 'new_last_uploaded_commit', 'parent',
Joanna Wang7603f042023-03-01 22:17:36 +00001049 'change_desc', 'prev_patchset'
Joanna Wange8523912023-01-21 02:05:40 +00001050])
1051
1052
Daniel Chengabf48472023-08-30 15:45:13 +00001053class ChangeDescription(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001054 """Contains a parsed form of the change description."""
1055 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
1056 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
1057 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
1058 FIXED_LINE = r'^[ \t]*Fixed[ \t]*:[ \t]*(.*?)[ \t]*$'
1059 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
1060 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
1061 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
1062 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):($|[^:])'
1063 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
Daniel Chengabf48472023-08-30 15:45:13 +00001064
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001065 def __init__(self, description, bug=None, fixed=None):
1066 self._description_lines = (description or '').strip().splitlines()
1067 if bug:
1068 regexp = re.compile(self.BUG_LINE)
1069 prefix = settings.GetBugPrefix()
1070 if not any(
1071 (regexp.match(line) for line in self._description_lines)):
1072 values = list(_get_bug_line_values(prefix, bug))
1073 self.append_footer('Bug: %s' % ', '.join(values))
1074 if fixed:
1075 regexp = re.compile(self.FIXED_LINE)
1076 prefix = settings.GetBugPrefix()
1077 if not any(
1078 (regexp.match(line) for line in self._description_lines)):
1079 values = list(_get_bug_line_values(prefix, fixed))
1080 self.append_footer('Fixed: %s' % ', '.join(values))
Daniel Chengabf48472023-08-30 15:45:13 +00001081
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001082 @property # www.logilab.org/ticket/89786
1083 def description(self): # pylint: disable=method-hidden
1084 return '\n'.join(self._description_lines)
Daniel Chengabf48472023-08-30 15:45:13 +00001085
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001086 def set_description(self, desc):
1087 if isinstance(desc, str):
1088 lines = desc.splitlines()
1089 else:
1090 lines = [line.rstrip() for line in desc]
1091 while lines and not lines[0]:
1092 lines.pop(0)
1093 while lines and not lines[-1]:
1094 lines.pop(-1)
1095 self._description_lines = lines
Daniel Chengabf48472023-08-30 15:45:13 +00001096
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001097 def ensure_change_id(self, change_id):
1098 description = self.description
1099 footer_change_ids = git_footers.get_footer_change_id(description)
1100 # Make sure that the Change-Id in the description matches the given one.
1101 if footer_change_ids != [change_id]:
1102 if footer_change_ids:
1103 # Remove any existing Change-Id footers since they don't match
1104 # the expected change_id footer.
1105 description = git_footers.remove_footer(description,
1106 'Change-Id')
1107 print(
1108 'WARNING: Change-Id has been set to %s. Use `git cl issue 0` '
1109 'if you want to set a new one.')
1110 # Add the expected Change-Id footer.
1111 description = git_footers.add_footer_change_id(
1112 description, change_id)
1113 self.set_description(description)
Daniel Chengabf48472023-08-30 15:45:13 +00001114
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001115 def update_reviewers(self, reviewers):
1116 """Rewrites the R= line(s) as a single line each.
Daniel Chengabf48472023-08-30 15:45:13 +00001117
1118 Args:
1119 reviewers (list(str)) - list of additional emails to use for reviewers.
1120 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001121 if not reviewers:
1122 return
Daniel Chengabf48472023-08-30 15:45:13 +00001123
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001124 reviewers = set(reviewers)
Daniel Chengabf48472023-08-30 15:45:13 +00001125
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001126 # Get the set of R= lines and remove them from the description.
1127 regexp = re.compile(self.R_LINE)
1128 matches = [regexp.match(line) for line in self._description_lines]
1129 new_desc = [
1130 l for i, l in enumerate(self._description_lines) if not matches[i]
1131 ]
1132 self.set_description(new_desc)
Daniel Chengabf48472023-08-30 15:45:13 +00001133
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001134 # Construct new unified R= lines.
Daniel Chengabf48472023-08-30 15:45:13 +00001135
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001136 # First, update reviewers with names from the R= lines (if any).
1137 for match in matches:
1138 if not match:
1139 continue
1140 reviewers.update(cleanup_list([match.group(2).strip()]))
Daniel Chengabf48472023-08-30 15:45:13 +00001141
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001142 new_r_line = 'R=' + ', '.join(sorted(reviewers))
Daniel Chengabf48472023-08-30 15:45:13 +00001143
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001144 # Put the new lines in the description where the old first R= line was.
1145 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1146 if 0 <= line_loc < len(self._description_lines):
1147 self._description_lines.insert(line_loc, new_r_line)
1148 else:
1149 self.append_footer(new_r_line)
Daniel Chengabf48472023-08-30 15:45:13 +00001150
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001151 def set_preserve_tryjobs(self):
1152 """Ensures description footer contains 'Cq-Do-Not-Cancel-Tryjobs: true'."""
1153 footers = git_footers.parse_footers(self.description)
1154 for v in footers.get('Cq-Do-Not-Cancel-Tryjobs', []):
1155 if v.lower() == 'true':
1156 return
1157 self.append_footer('Cq-Do-Not-Cancel-Tryjobs: true')
Daniel Chengabf48472023-08-30 15:45:13 +00001158
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001159 def prompt(self):
1160 """Asks the user to update the description."""
1161 self.set_description([
1162 '# Enter a description of the change.',
1163 '# This will be displayed on the codereview site.',
1164 '# The first line will also be used as the subject of the review.',
1165 '#--------------------This line is 72 characters long'
1166 '--------------------',
1167 ] + self._description_lines)
1168 bug_regexp = re.compile(self.BUG_LINE)
1169 fixed_regexp = re.compile(self.FIXED_LINE)
1170 prefix = settings.GetBugPrefix()
1171 has_issue = lambda l: bug_regexp.match(l) or fixed_regexp.match(l)
Daniel Chengabf48472023-08-30 15:45:13 +00001172
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001173 if not any((has_issue(line) for line in self._description_lines)):
1174 self.append_footer('Bug: %s' % prefix)
Daniel Chengabf48472023-08-30 15:45:13 +00001175
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001176 print('Waiting for editor...')
1177 content = gclient_utils.RunEditor(self.description,
1178 True,
1179 git_editor=settings.GetGitEditor())
1180 if not content:
1181 DieWithError('Running editor failed')
1182 lines = content.splitlines()
Daniel Chengabf48472023-08-30 15:45:13 +00001183
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001184 # Strip off comments and default inserted "Bug:" line.
1185 clean_lines = [
1186 line.rstrip() for line in lines
1187 if not (line.startswith('#') or line.rstrip() == "Bug:"
1188 or line.rstrip() == "Bug: " + prefix)
1189 ]
1190 if not clean_lines:
1191 DieWithError('No CL description, aborting')
1192 self.set_description(clean_lines)
Daniel Chengabf48472023-08-30 15:45:13 +00001193
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001194 def append_footer(self, line):
1195 """Adds a footer line to the description.
Daniel Chengabf48472023-08-30 15:45:13 +00001196
1197 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
1198 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
1199 that Gerrit footers are always at the end.
1200 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001201 parsed_footer_line = git_footers.parse_footer(line)
1202 if parsed_footer_line:
1203 # Line is a gerrit footer in the form: Footer-Key: any value.
1204 # Thus, must be appended observing Gerrit footer rules.
1205 self.set_description(
1206 git_footers.add_footer(self.description,
1207 key=parsed_footer_line[0],
1208 value=parsed_footer_line[1]))
1209 return
Daniel Chengabf48472023-08-30 15:45:13 +00001210
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001211 if not self._description_lines:
1212 self._description_lines.append(line)
1213 return
Daniel Chengabf48472023-08-30 15:45:13 +00001214
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001215 top_lines, gerrit_footers, _ = git_footers.split_footers(
1216 self.description)
1217 if gerrit_footers:
1218 # git_footers.split_footers ensures that there is an empty line
1219 # before actual (gerrit) footers, if any. We have to keep it that
1220 # way.
1221 assert top_lines and top_lines[-1] == ''
1222 top_lines, separator = top_lines[:-1], top_lines[-1:]
1223 else:
1224 separator = [
1225 ] # No need for separator if there are no gerrit_footers.
Daniel Chengabf48472023-08-30 15:45:13 +00001226
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001227 prev_line = top_lines[-1] if top_lines else ''
1228 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line)
1229 or not presubmit_support.Change.TAG_LINE_RE.match(line)):
1230 top_lines.append('')
1231 top_lines.append(line)
1232 self._description_lines = top_lines + separator + gerrit_footers
Daniel Chengabf48472023-08-30 15:45:13 +00001233
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001234 def get_reviewers(self, tbr_only=False):
1235 """Retrieves the list of reviewers."""
1236 matches = [
1237 re.match(self.R_LINE, line) for line in self._description_lines
1238 ]
1239 reviewers = [
1240 match.group(2).strip() for match in matches
1241 if match and (not tbr_only or match.group(1).upper() == 'TBR')
1242 ]
1243 return cleanup_list(reviewers)
Daniel Chengabf48472023-08-30 15:45:13 +00001244
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001245 def get_cced(self):
1246 """Retrieves the list of reviewers."""
1247 matches = [
1248 re.match(self.CC_LINE, line) for line in self._description_lines
1249 ]
1250 cced = [match.group(2).strip() for match in matches if match]
1251 return cleanup_list(cced)
Daniel Chengabf48472023-08-30 15:45:13 +00001252
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001253 def get_hash_tags(self):
1254 """Extracts and sanitizes a list of Gerrit hashtags."""
1255 subject = (self._description_lines or ('', ))[0]
1256 subject = re.sub(self.STRIP_HASH_TAG_PREFIX,
1257 '',
1258 subject,
1259 flags=re.IGNORECASE)
Daniel Chengabf48472023-08-30 15:45:13 +00001260
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001261 tags = []
1262 start = 0
1263 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
1264 while True:
1265 m = bracket_exp.match(subject, start)
1266 if not m:
1267 break
1268 tags.append(self.sanitize_hash_tag(m.group(1)))
1269 start = m.end()
Daniel Chengabf48472023-08-30 15:45:13 +00001270
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001271 if not tags:
1272 # Try "Tag: " prefix.
1273 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
1274 if m:
1275 tags.append(self.sanitize_hash_tag(m.group(1)))
1276 return tags
Daniel Chengabf48472023-08-30 15:45:13 +00001277
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001278 @classmethod
1279 def sanitize_hash_tag(cls, tag):
1280 """Returns a sanitized Gerrit hash tag.
Daniel Chengabf48472023-08-30 15:45:13 +00001281
1282 A sanitized hashtag can be used as a git push refspec parameter value.
1283 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001284 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
Daniel Chengabf48472023-08-30 15:45:13 +00001285
1286
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001287class Changelist(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001288 """Changelist works with one changelist in local branch.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001289
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001290 Notes:
1291 * Not safe for concurrent multi-{thread,process} use.
1292 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001293 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001294 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001295 def __init__(self,
1296 branchref=None,
1297 issue=None,
1298 codereview_host=None,
1299 commit_date=None):
1300 """Create a new ChangeList instance.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001301
Edward Lemurf38bc172019-09-03 21:02:13 +00001302 **kwargs will be passed directly to Gerrit implementation.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001303 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001304 # Poke settings so we get the "configure your server" message if
1305 # necessary.
1306 global settings
1307 if not settings:
1308 # Happens when git_cl.py is used as a utility library.
1309 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001310
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001311 self.branchref = branchref
1312 if self.branchref:
1313 assert branchref.startswith('refs/heads/')
1314 self.branch = scm.GIT.ShortBranchName(self.branchref)
1315 else:
1316 self.branch = None
1317 self.commit_date = commit_date
1318 self.upstream_branch = None
1319 self.lookedup_issue = False
1320 self.issue = issue or None
1321 self.description = None
1322 self.lookedup_patchset = False
1323 self.patchset = None
1324 self.cc = None
1325 self.more_cc = []
1326 self._remote = None
1327 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001328
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001329 # Lazily cached values.
1330 self._gerrit_host = None # e.g. chromium-review.googlesource.com
1331 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
1332 self._owners_client = None
1333 # Map from change number (issue) to its detail cache.
1334 self._detail_cache = {}
Edward Lemur125d60a2019-09-13 18:25:41 +00001335
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001336 if codereview_host is not None:
1337 assert not codereview_host.startswith('https://'), codereview_host
1338 self._gerrit_host = codereview_host
1339 self._gerrit_server = 'https://%s' % codereview_host
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001340
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001341 @property
1342 def owners_client(self):
1343 if self._owners_client is None:
1344 remote, remote_branch = self.GetRemoteBranch()
1345 branch = GetTargetRef(remote, remote_branch, None)
1346 self._owners_client = owners_client.GetCodeOwnersClient(
1347 host=self.GetGerritHost(),
1348 project=self.GetGerritProject(),
1349 branch=branch)
1350 return self._owners_client
Edward Lesmese1576912021-02-16 21:53:34 +00001351
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001352 def GetCCList(self):
1353 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001354
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001355 The return value is a string suitable for passing to git cl with the --cc
1356 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001357 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001358 if self.cc is None:
1359 base_cc = settings.GetDefaultCCList()
1360 more_cc = ','.join(self.more_cc)
1361 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1362 return self.cc
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001363
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001364 def ExtendCC(self, more_cc):
1365 """Extends the list of users to cc on this CL based on the changed files."""
1366 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001367
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001368 def GetCommitDate(self):
1369 """Returns the commit date as provided in the constructor"""
1370 return self.commit_date
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00001371
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001372 def GetBranch(self):
1373 """Returns the short branch name, e.g. 'main'."""
1374 if not self.branch:
1375 branchref = scm.GIT.GetBranchRef(settings.GetRoot())
1376 if not branchref:
1377 return None
1378 self.branchref = branchref
1379 self.branch = scm.GIT.ShortBranchName(self.branchref)
1380 return self.branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001381
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001382 def GetBranchRef(self):
1383 """Returns the full branch name, e.g. 'refs/heads/main'."""
1384 self.GetBranch() # Poke the lazy loader.
1385 return self.branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001386
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001387 def _GitGetBranchConfigValue(self, key, default=None):
1388 return scm.GIT.GetBranchConfig(settings.GetRoot(), self.GetBranch(),
1389 key, default)
tandrii5d48c322016-08-18 16:19:37 -07001390
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001391 def _GitSetBranchConfigValue(self, key, value):
1392 action = 'set %s to %r' % (key, value)
1393 if not value:
1394 action = 'unset %s' % key
1395 assert self.GetBranch(), 'a branch is needed to ' + action
1396 return scm.GIT.SetBranchConfig(settings.GetRoot(), self.GetBranch(),
1397 key, value)
tandrii5d48c322016-08-18 16:19:37 -07001398
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001399 @staticmethod
1400 def FetchUpstreamTuple(branch):
1401 """Returns a tuple containing remote and remote ref,
Josip Sokcevicc39ab992020-09-24 20:09:15 +00001402 e.g. 'origin', 'refs/heads/main'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001403 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001404 remote, upstream_branch = scm.GIT.FetchUpstreamTuple(
1405 settings.GetRoot(), branch)
1406 if not remote or not upstream_branch:
1407 DieWithError(
1408 'Unable to determine default branch to diff against.\n'
1409 'Verify this branch is set up to track another \n'
1410 '(via the --track argument to "git checkout -b ..."). \n'
1411 'or pass complete "git diff"-style arguments if supported, like\n'
1412 ' git cl upload origin/main\n')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001413
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001414 return remote, upstream_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001415
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001416 def GetCommonAncestorWithUpstream(self):
1417 upstream_branch = self.GetUpstreamBranch()
1418 if not scm.GIT.IsValidRevision(settings.GetRoot(), upstream_branch):
1419 DieWithError(
Joanna Wangd4dfff02023-09-13 17:44:31 +00001420 'The current branch (%s) has an upstream (%s) that does not exist '
1421 'anymore.\nPlease fix it and try again.' %
1422 (self.GetBranch(), upstream_branch))
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001423 return git_common.get_or_create_merge_base(self.GetBranch(),
1424 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001425
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001426 def GetUpstreamBranch(self):
1427 if self.upstream_branch is None:
1428 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
1429 if remote != '.':
1430 upstream_branch = upstream_branch.replace(
1431 'refs/heads/', 'refs/remotes/%s/' % remote)
1432 upstream_branch = upstream_branch.replace(
1433 'refs/branch-heads/', 'refs/remotes/branch-heads/')
1434 self.upstream_branch = upstream_branch
1435 return self.upstream_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001436
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001437 def GetRemoteBranch(self):
1438 if not self._remote:
1439 remote, branch = None, self.GetBranch()
1440 seen_branches = set()
1441 while branch not in seen_branches:
1442 seen_branches.add(branch)
1443 remote, branch = self.FetchUpstreamTuple(branch)
1444 branch = scm.GIT.ShortBranchName(branch)
1445 if remote != '.' or branch.startswith('refs/remotes'):
1446 break
1447 else:
1448 remotes = RunGit(['remote'], error_ok=True).split()
1449 if len(remotes) == 1:
1450 remote, = remotes
1451 elif 'origin' in remotes:
1452 remote = 'origin'
1453 logging.warning(
1454 'Could not determine which remote this change is '
1455 'associated with, so defaulting to "%s".' %
1456 self._remote)
1457 else:
1458 logging.warning(
1459 'Could not determine which remote this change is '
1460 'associated with.')
1461 branch = 'HEAD'
1462 if branch.startswith('refs/remotes'):
1463 self._remote = (remote, branch)
1464 elif branch.startswith('refs/branch-heads/'):
1465 self._remote = (remote, branch.replace('refs/',
1466 'refs/remotes/'))
1467 else:
1468 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
1469 return self._remote
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001470
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001471 def GetRemoteUrl(self) -> Optional[str]:
1472 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001473
1474 Returns None if there is no remote.
1475 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001476 is_cached, value = self._cached_remote_url
1477 if is_cached:
1478 return value
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001479
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001480 remote, _ = self.GetRemoteBranch()
1481 url = scm.GIT.GetConfig(settings.GetRoot(), 'remote.%s.url' % remote,
1482 '')
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001483
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001484 # Check if the remote url can be parsed as an URL.
1485 host = urllib.parse.urlparse(url).netloc
1486 if host:
1487 self._cached_remote_url = (True, url)
1488 return url
Edward Lemur298f2cf2019-02-22 21:40:39 +00001489
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001490 # If it cannot be parsed as an url, assume it is a local directory,
1491 # probably a git cache.
1492 logging.warning(
1493 '"%s" doesn\'t appear to point to a git host. '
1494 'Interpreting it as a local directory.', url)
1495 if not os.path.isdir(url):
1496 logging.error(
1497 'Remote "%(remote)s" for branch "%(branch)s" points to "%(url)s", '
1498 'but it doesn\'t exist.', {
1499 'remote': remote,
1500 'branch': self.GetBranch(),
1501 'url': url
1502 })
1503 return None
Edward Lemur298f2cf2019-02-22 21:40:39 +00001504
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001505 cache_path = url
1506 url = scm.GIT.GetConfig(url, 'remote.%s.url' % remote, '')
Edward Lemur298f2cf2019-02-22 21:40:39 +00001507
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001508 host = urllib.parse.urlparse(url).netloc
1509 if not host:
1510 logging.error(
1511 'Remote "%(remote)s" for branch "%(branch)s" points to '
1512 '"%(cache_path)s", but it is misconfigured.\n'
1513 '"%(cache_path)s" must be a git repo and must have a remote named '
1514 '"%(remote)s" pointing to the git host.', {
1515 'remote': remote,
1516 'cache_path': cache_path,
1517 'branch': self.GetBranch()
1518 })
1519 return None
Edward Lemur298f2cf2019-02-22 21:40:39 +00001520
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001521 self._cached_remote_url = (True, url)
1522 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001523
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001524 def GetIssue(self):
1525 """Returns the issue number as a int or None if not set."""
1526 if self.issue is None and not self.lookedup_issue:
1527 if self.GetBranch():
1528 self.issue = self._GitGetBranchConfigValue(ISSUE_CONFIG_KEY)
1529 if self.issue is not None:
1530 self.issue = int(self.issue)
1531 self.lookedup_issue = True
1532 return self.issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001533
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001534 def GetIssueURL(self, short=False):
1535 """Get the URL for a particular issue."""
1536 issue = self.GetIssue()
1537 if not issue:
1538 return None
1539 server = self.GetCodereviewServer()
1540 if short:
1541 server = _KNOWN_GERRIT_TO_SHORT_URLS.get(server, server)
1542 return '%s/%s' % (server, issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001543
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001544 def FetchDescription(self, pretty=False):
1545 assert self.GetIssue(), 'issue is required to query Gerrit'
Edward Lemur6c6827c2020-02-06 21:15:18 +00001546
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001547 if self.description is None:
1548 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
1549 current_rev = data['current_revision']
1550 self.description = data['revisions'][current_rev]['commit'][
1551 'message']
Edward Lemur6c6827c2020-02-06 21:15:18 +00001552
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001553 if not pretty:
1554 return self.description
Edward Lemur6c6827c2020-02-06 21:15:18 +00001555
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001556 # Set width to 72 columns + 2 space indent.
1557 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
1558 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1559 lines = self.description.splitlines()
1560 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001561
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001562 def GetPatchset(self):
1563 """Returns the patchset number as a int or None if not set."""
1564 if self.patchset is None and not self.lookedup_patchset:
1565 if self.GetBranch():
1566 self.patchset = self._GitGetBranchConfigValue(
1567 PATCHSET_CONFIG_KEY)
1568 if self.patchset is not None:
1569 self.patchset = int(self.patchset)
1570 self.lookedup_patchset = True
1571 return self.patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001572
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001573 def GetAuthor(self):
1574 return scm.GIT.GetConfig(settings.GetRoot(), 'user.email')
Edward Lemur9aa1a962020-02-25 00:58:38 +00001575
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001576 def SetPatchset(self, patchset):
1577 """Set this branch's patchset. If patchset=0, clears the patchset."""
1578 assert self.GetBranch()
1579 if not patchset:
1580 self.patchset = None
1581 else:
1582 self.patchset = int(patchset)
1583 self._GitSetBranchConfigValue(PATCHSET_CONFIG_KEY, str(self.patchset))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001584
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001585 def SetIssue(self, issue=None):
1586 """Set this branch's issue. If issue isn't given, clears the issue."""
1587 assert self.GetBranch()
1588 if issue:
1589 issue = int(issue)
1590 self._GitSetBranchConfigValue(ISSUE_CONFIG_KEY, str(issue))
1591 self.issue = issue
1592 codereview_server = self.GetCodereviewServer()
1593 if codereview_server:
1594 self._GitSetBranchConfigValue(CODEREVIEW_SERVER_CONFIG_KEY,
1595 codereview_server)
1596 else:
1597 # Reset all of these just to be clean.
1598 reset_suffixes = [
1599 LAST_UPLOAD_HASH_CONFIG_KEY,
1600 ISSUE_CONFIG_KEY,
1601 PATCHSET_CONFIG_KEY,
1602 CODEREVIEW_SERVER_CONFIG_KEY,
1603 GERRIT_SQUASH_HASH_CONFIG_KEY,
1604 ]
1605 for prop in reset_suffixes:
1606 try:
1607 self._GitSetBranchConfigValue(prop, None)
1608 except subprocess2.CalledProcessError:
1609 pass
1610 msg = RunGit(['log', '-1', '--format=%B']).strip()
1611 if msg and git_footers.get_footer_change_id(msg):
1612 print(
1613 'WARNING: The change patched into this branch has a Change-Id. '
1614 'Removing it.')
1615 RunGit([
1616 'commit', '--amend', '-m',
1617 git_footers.remove_footer(msg, 'Change-Id')
1618 ])
1619 self.lookedup_issue = True
1620 self.issue = None
1621 self.patchset = None
1622
1623 def GetAffectedFiles(self,
1624 upstream: str,
1625 end_commit: Optional[str] = None) -> Sequence[str]:
1626 """Returns the list of affected files for the given commit range."""
Edward Lemur85153282020-02-14 22:06:29 +00001627 try:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001628 return [
1629 f for _, f in scm.GIT.CaptureStatus(
1630 settings.GetRoot(), upstream, end_commit=end_commit)
1631 ]
Edward Lemur85153282020-02-14 22:06:29 +00001632 except subprocess2.CalledProcessError:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001633 DieWithError(
1634 ('\nFailed to diff against upstream branch %s\n\n'
1635 'This branch probably doesn\'t exist anymore. To reset the\n'
1636 'tracking branch, please run\n'
1637 ' git branch --set-upstream-to origin/main %s\n'
1638 'or replace origin/main with the relevant branch') %
1639 (upstream, self.GetBranch()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001640
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001641 def UpdateDescription(self, description, force=False):
1642 assert self.GetIssue(), 'issue is required to update description'
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001643
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001644 if gerrit_util.HasPendingChangeEdit(self.GetGerritHost(),
1645 self._GerritChangeIdentifier()):
1646 if not force:
1647 confirm_or_exit(
1648 'The description cannot be modified while the issue has a pending '
1649 'unpublished edit. Either publish the edit in the Gerrit web UI '
1650 'or delete it.\n\n',
1651 action='delete the unpublished edit')
Edward Lemur6c6827c2020-02-06 21:15:18 +00001652
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001653 gerrit_util.DeletePendingChangeEdit(self.GetGerritHost(),
1654 self._GerritChangeIdentifier())
1655 gerrit_util.SetCommitMessage(self.GetGerritHost(),
1656 self._GerritChangeIdentifier(),
1657 description,
1658 notify='NONE')
Edward Lemur6c6827c2020-02-06 21:15:18 +00001659
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001660 self.description = description
Edward Lemur6c6827c2020-02-06 21:15:18 +00001661
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001662 def _GetCommonPresubmitArgs(self, verbose, upstream):
1663 args = [
1664 '--root',
1665 settings.GetRoot(),
1666 '--upstream',
1667 upstream,
1668 ]
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001669
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001670 args.extend(['--verbose'] * verbose)
Edward Lemur227d5102020-02-25 23:45:35 +00001671
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001672 remote, remote_branch = self.GetRemoteBranch()
1673 target_ref = GetTargetRef(remote, remote_branch, None)
1674 if settings.GetIsGerrit():
1675 args.extend(['--gerrit_url', self.GetCodereviewServer()])
1676 args.extend(['--gerrit_project', self.GetGerritProject()])
1677 args.extend(['--gerrit_branch', target_ref])
Edward Lemur227d5102020-02-25 23:45:35 +00001678
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001679 author = self.GetAuthor()
1680 issue = self.GetIssue()
1681 patchset = self.GetPatchset()
1682 if author:
1683 args.extend(['--author', author])
1684 if issue:
1685 args.extend(['--issue', str(issue)])
1686 if patchset:
1687 args.extend(['--patchset', str(patchset)])
Edward Lesmeseb1bd622021-03-01 19:54:07 +00001688
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001689 return args
Edward Lemur227d5102020-02-25 23:45:35 +00001690
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001691 def RunHook(self,
1692 committing,
1693 may_prompt,
1694 verbose,
1695 parallel,
1696 upstream,
1697 description,
1698 all_files,
1699 files=None,
1700 resultdb=False,
1701 realm=None):
1702 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1703 args = self._GetCommonPresubmitArgs(verbose, upstream)
1704 args.append('--commit' if committing else '--upload')
1705 if may_prompt:
1706 args.append('--may_prompt')
1707 if parallel:
1708 args.append('--parallel')
1709 if all_files:
1710 args.append('--all_files')
1711 if files:
1712 args.extend(files.split(';'))
1713 args.append('--source_controlled_only')
1714 if files or all_files:
1715 args.append('--no_diffs')
Edward Lemur75526302020-02-27 22:31:05 +00001716
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001717 if resultdb and not realm:
1718 # TODO (crbug.com/1113463): store realm somewhere and look it up so
1719 # it is not required to pass the realm flag
1720 print(
1721 'Note: ResultDB reporting will NOT be performed because --realm'
1722 ' was not specified. To enable ResultDB, please run the command'
1723 ' again with the --realm argument to specify the LUCI realm.')
Edward Lemur227d5102020-02-25 23:45:35 +00001724
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001725 return self._RunPresubmit(args,
1726 description,
1727 resultdb=resultdb,
1728 realm=realm)
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001729
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001730 def _RunPresubmit(self,
1731 args: Sequence[str],
1732 description: str,
1733 resultdb: bool = False,
1734 realm: Optional[str] = None) -> Mapping[str, Any]:
1735 args = list(args)
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001736
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001737 with gclient_utils.temporary_file() as description_file:
1738 with gclient_utils.temporary_file() as json_output:
1739 gclient_utils.FileWrite(description_file, description)
1740 args.extend(['--json_output', json_output])
1741 args.extend(['--description_file', description_file])
1742 start = time_time()
1743 cmd = ['vpython3', PRESUBMIT_SUPPORT] + args
1744 if resultdb and realm:
1745 cmd = ['rdb', 'stream', '-new', '-realm', realm, '--'] + cmd
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001746
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001747 p = subprocess2.Popen(cmd)
1748 exit_code = p.wait()
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001749
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001750 metrics.collector.add_repeated(
1751 'sub_commands', {
1752 'command': 'presubmit',
1753 'execution_time': time_time() - start,
1754 'exit_code': exit_code,
1755 })
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001756
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001757 if exit_code:
1758 sys.exit(exit_code)
Edward Lemur227d5102020-02-25 23:45:35 +00001759
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001760 json_results = gclient_utils.FileRead(json_output)
1761 return json.loads(json_results)
Edward Lemur227d5102020-02-25 23:45:35 +00001762
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001763 def RunPostUploadHook(self, verbose, upstream, description):
1764 args = self._GetCommonPresubmitArgs(verbose, upstream)
1765 args.append('--post_upload')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001766
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001767 with gclient_utils.temporary_file() as description_file:
1768 gclient_utils.FileWrite(description_file, description)
1769 args.extend(['--description_file', description_file])
1770 subprocess2.Popen(['vpython3', PRESUBMIT_SUPPORT] + args).wait()
Edward Lemur75526302020-02-27 22:31:05 +00001771
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001772 def _GetDescriptionForUpload(self, options: optparse.Values,
1773 git_diff_args: Sequence[str],
1774 files: Sequence[str]) -> ChangeDescription:
1775 """Get description message for upload."""
1776 if self.GetIssue():
1777 description = self.FetchDescription()
1778 elif options.message:
1779 description = options.message
1780 else:
1781 description = _create_description_from_log(git_diff_args)
1782 if options.title and options.squash:
1783 description = options.title + '\n\n' + description
Edward Lemur75526302020-02-27 22:31:05 +00001784
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001785 bug = options.bug
1786 fixed = options.fixed
1787 if not self.GetIssue():
1788 # Extract bug number from branch name, but only if issue is being
1789 # created. It must start with bug or fix, followed by _ or - and
1790 # number. Optionally, it may contain _ or - after number with
1791 # arbitrary text. Examples: bug-123 bug_123 fix-123
1792 # fix-123-some-description
1793 branch = self.GetBranch()
1794 if branch is not None:
1795 match = re.match(
1796 r'^(?P<type>bug|fix(?:e[sd])?)[_-]?(?P<bugnum>\d+)([-_]|$)',
1797 branch)
1798 if not bug and not fixed and match:
1799 if match.group('type') == 'bug':
1800 bug = match.group('bugnum')
1801 else:
1802 fixed = match.group('bugnum')
Edward Lemur5a644f82020-03-18 16:44:57 +00001803
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001804 change_description = ChangeDescription(description, bug, fixed)
Edward Lemur5a644f82020-03-18 16:44:57 +00001805
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001806 # Fill gaps in OWNERS coverage to reviewers if requested.
1807 if options.add_owners_to:
1808 assert options.add_owners_to in ('R'), options.add_owners_to
1809 status = self.owners_client.GetFilesApprovalStatus(
1810 files, [], options.reviewers)
1811 missing_files = [
1812 f for f in files
1813 if status[f] == self._owners_client.INSUFFICIENT_REVIEWERS
1814 ]
1815 owners = self.owners_client.SuggestOwners(
1816 missing_files, exclude=[self.GetAuthor()])
1817 assert isinstance(options.reviewers, list), options.reviewers
1818 options.reviewers.extend(owners)
Edward Lemur5a644f82020-03-18 16:44:57 +00001819
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001820 # Set the reviewer list now so that presubmit checks can access it.
1821 if options.reviewers:
1822 change_description.update_reviewers(options.reviewers)
Edward Lesmes8c43c3f2021-01-20 00:20:26 +00001823
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001824 return change_description
Edward Lemur5a644f82020-03-18 16:44:57 +00001825
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001826 def _GetTitleForUpload(self, options, multi_change_upload=False):
1827 # type: (optparse.Values, Optional[bool]) -> str
Edward Lemur5a644f82020-03-18 16:44:57 +00001828
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001829 # Getting titles for multipl commits is not supported so we return the
1830 # default.
1831 if not options.squash or multi_change_upload or options.title:
1832 return options.title
Joanna Wanga1abbed2023-01-24 01:41:05 +00001833
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001834 # On first upload, patchset title is always this string, while
1835 # options.title gets converted to first line of message.
1836 if not self.GetIssue():
1837 return 'Initial upload'
Edward Lemur5a644f82020-03-18 16:44:57 +00001838
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001839 # When uploading subsequent patchsets, options.message is taken as the
1840 # title if options.title is not provided.
1841 if options.message:
1842 return options.message.strip()
Edward Lemur5a644f82020-03-18 16:44:57 +00001843
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001844 # Use the subject of the last commit as title by default.
1845 title = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
1846 if options.force or options.skip_title:
1847 return title
1848 user_title = gclient_utils.AskForData('Title for patchset [%s]: ' %
1849 title)
Edward Lemur5a644f82020-03-18 16:44:57 +00001850
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001851 # Use the default title if the user confirms the default with a 'y'.
1852 if user_title.lower() == 'y':
1853 return title
1854 return user_title or title
mlcui3da91712021-05-05 10:00:30 +00001855
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001856 def _GetRefSpecOptions(self,
1857 options: optparse.Values,
1858 change_desc: ChangeDescription,
1859 multi_change_upload: bool = False,
1860 dogfood_path: bool = False) -> List[str]:
1861 # Extra options that can be specified at push time. Doc:
1862 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
1863 refspec_opts = []
Edward Lemur5a644f82020-03-18 16:44:57 +00001864
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001865 # By default, new changes are started in WIP mode, and subsequent
1866 # patchsets don't send email. At any time, passing --send-mail or
1867 # --send-email will mark the change ready and send email for that
1868 # particular patch.
1869 if options.send_mail:
1870 refspec_opts.append('ready')
1871 refspec_opts.append('notify=ALL')
1872 elif (not self.GetIssue() and options.squash and not dogfood_path):
1873 refspec_opts.append('wip')
Joanna Wanga1abbed2023-01-24 01:41:05 +00001874
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001875 # TODO(tandrii): options.message should be posted as a comment if
1876 # --send-mail or --send-email is set on non-initial upload as Rietveld
1877 # used to do it.
Joanna Wanga1abbed2023-01-24 01:41:05 +00001878
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001879 # Set options.title in case user was prompted in _GetTitleForUpload and
1880 # _CMDUploadChange needs to be called again.
1881 options.title = self._GetTitleForUpload(
1882 options, multi_change_upload=multi_change_upload)
Joanna Wanga1abbed2023-01-24 01:41:05 +00001883
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001884 if options.title:
1885 # Punctuation and whitespace in |title| must be percent-encoded.
1886 refspec_opts.append(
1887 'm=' + gerrit_util.PercentEncodeForGitRef(options.title))
Joanna Wanga1abbed2023-01-24 01:41:05 +00001888
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001889 if options.private:
1890 refspec_opts.append('private')
Joanna Wanga1abbed2023-01-24 01:41:05 +00001891
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001892 if options.topic:
1893 # Documentation on Gerrit topics is here:
1894 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
1895 refspec_opts.append('topic=%s' % options.topic)
Joanna Wanga1abbed2023-01-24 01:41:05 +00001896
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001897 if options.enable_auto_submit:
1898 refspec_opts.append('l=Auto-Submit+1')
1899 if options.set_bot_commit:
1900 refspec_opts.append('l=Bot-Commit+1')
1901 if options.use_commit_queue:
1902 refspec_opts.append('l=Commit-Queue+2')
1903 elif options.cq_dry_run:
1904 refspec_opts.append('l=Commit-Queue+1')
Joanna Wanga1abbed2023-01-24 01:41:05 +00001905
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001906 if change_desc.get_reviewers(tbr_only=True):
1907 score = gerrit_util.GetCodeReviewTbrScore(self.GetGerritHost(),
1908 self.GetGerritProject())
1909 refspec_opts.append('l=Code-Review+%s' % score)
Joanna Wanga1abbed2023-01-24 01:41:05 +00001910
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001911 # Gerrit sorts hashtags, so order is not important.
1912 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
1913 # We check GetIssue because we only add hashtags from the
1914 # description on the first upload.
1915 # TODO(b/265929888): When we fully launch the new path:
1916 # 1) remove fetching hashtags from description alltogether
1917 # 2) Or use descrtiption hashtags for:
1918 # `not (self.GetIssue() and multi_change_upload)`
1919 # 3) Or enabled change description tags for multi and single changes
1920 # by adding them post `git push`.
1921 if not (self.GetIssue() and dogfood_path):
1922 hashtags.update(change_desc.get_hash_tags())
1923 refspec_opts.extend(['hashtag=%s' % t for t in hashtags])
Joanna Wanga1abbed2023-01-24 01:41:05 +00001924
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001925 # Note: Reviewers, and ccs are handled individually for each
1926 # branch/change.
1927 return refspec_opts
Joanna Wang40497912023-01-24 21:18:16 +00001928
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001929 def PrepareSquashedCommit(self,
1930 options: optparse.Values,
1931 parent: str,
1932 orig_parent: str,
1933 end_commit: Optional[str] = None) -> _NewUpload:
1934 """Create a squashed commit to upload.
Joanna Wang05b60342023-03-29 20:25:57 +00001935
1936
1937 Args:
1938 parent: The commit to use as the parent for the new squashed.
1939 orig_parent: The commit that is an actual ancestor of `end_commit`. It
1940 is part of the same original tree as end_commit, which does not
1941 contain squashed commits. This is used to create the change
1942 description for the new squashed commit with:
1943 `git log orig_parent..end_commit`.
1944 end_commit: The commit to use as the end of the new squashed commit.
1945 """
Joanna Wangb88a4342023-01-24 01:28:22 +00001946
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001947 if end_commit is None:
1948 end_commit = RunGit(['rev-parse', self.branchref]).strip()
Joanna Wangb88a4342023-01-24 01:28:22 +00001949
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001950 reviewers, ccs, change_desc = self._PrepareChange(
1951 options, orig_parent, end_commit)
1952 latest_tree = RunGit(['rev-parse', end_commit + ':']).strip()
1953 with gclient_utils.temporary_file() as desc_tempfile:
1954 gclient_utils.FileWrite(desc_tempfile, change_desc.description)
1955 commit_to_push = RunGit(
1956 ['commit-tree', latest_tree, '-p', parent, '-F',
1957 desc_tempfile]).strip()
Joanna Wangb88a4342023-01-24 01:28:22 +00001958
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001959 # Gerrit may or may not update fast enough to return the correct
1960 # patchset number after we push. Get the pre-upload patchset and
1961 # increment later.
1962 prev_patchset = self.GetMostRecentPatchset(update=False) or 0
1963 return _NewUpload(reviewers, ccs, commit_to_push, end_commit, parent,
1964 change_desc, prev_patchset)
Joanna Wangb88a4342023-01-24 01:28:22 +00001965
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001966 def PrepareCherryPickSquashedCommit(self, options: optparse.Values,
1967 parent: str) -> _NewUpload:
1968 """Create a commit cherry-picked on parent to push."""
Joanna Wange8523912023-01-21 02:05:40 +00001969
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001970 # The `parent` is what we will cherry-pick on top of.
1971 # The `cherry_pick_base` is the beginning range of what
1972 # we are cherry-picking.
1973 cherry_pick_base = self.GetCommonAncestorWithUpstream()
1974 reviewers, ccs, change_desc = self._PrepareChange(
1975 options, cherry_pick_base, self.branchref)
Joanna Wange8523912023-01-21 02:05:40 +00001976
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001977 new_upload_hash = RunGit(['rev-parse', self.branchref]).strip()
1978 latest_tree = RunGit(['rev-parse', self.branchref + ':']).strip()
1979 with gclient_utils.temporary_file() as desc_tempfile:
1980 gclient_utils.FileWrite(desc_tempfile, change_desc.description)
1981 commit_to_cp = RunGit([
1982 'commit-tree', latest_tree, '-p', cherry_pick_base, '-F',
1983 desc_tempfile
1984 ]).strip()
Joanna Wange8523912023-01-21 02:05:40 +00001985
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001986 RunGit(['checkout', '-q', parent])
1987 ret, _out = RunGitWithCode(['cherry-pick', commit_to_cp])
1988 if ret:
1989 RunGit(['cherry-pick', '--abort'])
1990 RunGit(['checkout', '-q', self.branch])
1991 DieWithError('Could not cleanly cherry-pick')
Joanna Wange8523912023-01-21 02:05:40 +00001992
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001993 commit_to_push = RunGit(['rev-parse', 'HEAD']).strip()
1994 RunGit(['checkout', '-q', self.branch])
Joanna Wange8523912023-01-21 02:05:40 +00001995
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001996 # Gerrit may or may not update fast enough to return the correct
1997 # patchset number after we push. Get the pre-upload patchset and
1998 # increment later.
1999 prev_patchset = self.GetMostRecentPatchset(update=False) or 0
2000 return _NewUpload(reviewers, ccs, commit_to_push, new_upload_hash,
2001 cherry_pick_base, change_desc, prev_patchset)
Joanna Wange8523912023-01-21 02:05:40 +00002002
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002003 def _PrepareChange(
2004 self, options: optparse.Values, parent: str, end_commit: str
2005 ) -> Tuple[Sequence[str], Sequence[str], ChangeDescription]:
2006 """Prepares the change to be uploaded."""
2007 self.EnsureCanUploadPatchset(options.force)
Joanna Wangb46232e2023-01-21 01:58:46 +00002008
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002009 files = self.GetAffectedFiles(parent, end_commit=end_commit)
2010 change_desc = self._GetDescriptionForUpload(options,
2011 [parent, end_commit], files)
Joanna Wangb46232e2023-01-21 01:58:46 +00002012
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002013 watchlist = watchlists.Watchlists(settings.GetRoot())
2014 self.ExtendCC(watchlist.GetWatchersForPaths(files))
2015 if not options.bypass_hooks:
2016 hook_results = self.RunHook(committing=False,
2017 may_prompt=not options.force,
2018 verbose=options.verbose,
2019 parallel=options.parallel,
2020 upstream=parent,
2021 description=change_desc.description,
2022 all_files=False)
2023 self.ExtendCC(hook_results['more_cc'])
Joanna Wangb46232e2023-01-21 01:58:46 +00002024
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002025 # Update the change description and ensure we have a Change Id.
2026 if self.GetIssue():
2027 if options.edit_description:
2028 change_desc.prompt()
2029 change_detail = self._GetChangeDetail(['CURRENT_REVISION'])
2030 change_id = change_detail['change_id']
2031 change_desc.ensure_change_id(change_id)
Joanna Wangb46232e2023-01-21 01:58:46 +00002032
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002033 else: # No change issue. First time uploading
2034 if not options.force and not options.message_file:
2035 change_desc.prompt()
Joanna Wangb46232e2023-01-21 01:58:46 +00002036
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002037 # Check if user added a change_id in the descripiton.
2038 change_ids = git_footers.get_footer_change_id(
2039 change_desc.description)
2040 if len(change_ids) == 1:
2041 change_id = change_ids[0]
2042 else:
2043 change_id = GenerateGerritChangeId(change_desc.description)
2044 change_desc.ensure_change_id(change_id)
Joanna Wangb46232e2023-01-21 01:58:46 +00002045
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002046 if options.preserve_tryjobs:
2047 change_desc.set_preserve_tryjobs()
Joanna Wangb46232e2023-01-21 01:58:46 +00002048
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002049 SaveDescriptionBackup(change_desc)
Joanna Wangb46232e2023-01-21 01:58:46 +00002050
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002051 # Add ccs
2052 ccs = []
2053 # Add default, watchlist, presubmit ccs if this is the initial upload
2054 # and CL is not private and auto-ccing has not been disabled.
2055 if not options.private and not options.no_autocc and not self.GetIssue(
2056 ):
2057 ccs = self.GetCCList().split(',')
2058 if len(ccs) > 100:
2059 lsc = (
2060 'https://chromium.googlesource.com/chromium/src/+/HEAD/docs/'
2061 'process/lsc/lsc_workflow.md')
2062 print('WARNING: This will auto-CC %s users.' % len(ccs))
2063 print('LSC may be more appropriate: %s' % lsc)
2064 print(
2065 'You can also use the --no-autocc flag to disable auto-CC.')
2066 confirm_or_exit(action='continue')
Joanna Wangb46232e2023-01-21 01:58:46 +00002067
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002068 # Add ccs from the --cc flag.
2069 if options.cc:
2070 ccs.extend(options.cc)
Joanna Wangb46232e2023-01-21 01:58:46 +00002071
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002072 ccs = [email.strip() for email in ccs if email.strip()]
2073 if change_desc.get_cced():
2074 ccs.extend(change_desc.get_cced())
Joanna Wangb46232e2023-01-21 01:58:46 +00002075
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002076 return change_desc.get_reviewers(), ccs, change_desc
Joanna Wangb46232e2023-01-21 01:58:46 +00002077
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002078 def PostUploadUpdates(self, options: optparse.Values,
2079 new_upload: _NewUpload, change_number: str) -> None:
2080 """Makes necessary post upload changes to the local and remote cl."""
2081 if not self.GetIssue():
2082 self.SetIssue(change_number)
Joanna Wang40497912023-01-24 21:18:16 +00002083
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002084 self.SetPatchset(new_upload.prev_patchset + 1)
Joanna Wang7603f042023-03-01 22:17:36 +00002085
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002086 self._GitSetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY,
2087 new_upload.commit_to_push)
2088 self._GitSetBranchConfigValue(LAST_UPLOAD_HASH_CONFIG_KEY,
2089 new_upload.new_last_uploaded_commit)
Joanna Wang40497912023-01-24 21:18:16 +00002090
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002091 if settings.GetRunPostUploadHook():
2092 self.RunPostUploadHook(options.verbose, new_upload.parent,
2093 new_upload.change_desc.description)
Joanna Wang40497912023-01-24 21:18:16 +00002094
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002095 if new_upload.reviewers or new_upload.ccs:
2096 gerrit_util.AddReviewers(self.GetGerritHost(),
2097 self._GerritChangeIdentifier(),
2098 reviewers=new_upload.reviewers,
2099 ccs=new_upload.ccs,
2100 notify=bool(options.send_mail))
Joanna Wang40497912023-01-24 21:18:16 +00002101
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002102 def CMDUpload(self, options, git_diff_args, orig_args):
2103 """Uploads a change to codereview."""
2104 custom_cl_base = None
2105 if git_diff_args:
2106 custom_cl_base = base_branch = git_diff_args[0]
2107 else:
2108 if self.GetBranch() is None:
2109 DieWithError(
2110 'Can\'t upload from detached HEAD state. Get on a branch!')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002111
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002112 # Default to diffing against common ancestor of upstream branch
2113 base_branch = self.GetCommonAncestorWithUpstream()
2114 git_diff_args = [base_branch, 'HEAD']
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002115
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002116 # Fast best-effort checks to abort before running potentially expensive
2117 # hooks if uploading is likely to fail anyway. Passing these checks does
2118 # not guarantee that uploading will not fail.
2119 self.EnsureAuthenticated(force=options.force)
2120 self.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002121
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002122 print(f'Processing {_GetCommitCountSummary(*git_diff_args)}...')
Daniel Cheng66d0f152023-08-29 23:21:58 +00002123
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002124 # Apply watchlists on upload.
2125 watchlist = watchlists.Watchlists(settings.GetRoot())
2126 files = self.GetAffectedFiles(base_branch)
2127 if not options.bypass_watchlists:
2128 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002129
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002130 change_desc = self._GetDescriptionForUpload(options, git_diff_args,
2131 files)
2132 if not options.bypass_hooks:
2133 hook_results = self.RunHook(committing=False,
2134 may_prompt=not options.force,
2135 verbose=options.verbose,
2136 parallel=options.parallel,
2137 upstream=base_branch,
2138 description=change_desc.description,
2139 all_files=False)
2140 self.ExtendCC(hook_results['more_cc'])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002141
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002142 print_stats(git_diff_args)
2143 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base,
2144 change_desc)
2145 if not ret:
2146 if self.GetBranch() is not None:
2147 self._GitSetBranchConfigValue(
2148 LAST_UPLOAD_HASH_CONFIG_KEY,
2149 scm.GIT.ResolveCommit(settings.GetRoot(), 'HEAD'))
2150 # Run post upload hooks, if specified.
2151 if settings.GetRunPostUploadHook():
2152 self.RunPostUploadHook(options.verbose, base_branch,
2153 change_desc.description)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002154
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002155 # Upload all dependencies if specified.
2156 if options.dependencies:
2157 print()
2158 print('--dependencies has been specified.')
2159 print('All dependent local branches will be re-uploaded.')
2160 print()
2161 # Remove the dependencies flag from args so that we do not end
2162 # up in a loop.
2163 orig_args.remove('--dependencies')
2164 ret = upload_branch_deps(self, orig_args, options.force)
2165 return ret
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002166
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002167 def SetCQState(self, new_state):
2168 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002169
Struan Shrimpton8b2072b2023-07-31 21:01:26 +00002170 Issue must have been already uploaded and known.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002171 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002172 assert new_state in _CQState.ALL_STATES
2173 assert self.GetIssue()
2174 try:
2175 vote_map = {
2176 _CQState.NONE: 0,
2177 _CQState.DRY_RUN: 1,
2178 _CQState.COMMIT: 2,
2179 }
2180 labels = {'Commit-Queue': vote_map[new_state]}
2181 notify = False if new_state == _CQState.DRY_RUN else None
2182 gerrit_util.SetReview(self.GetGerritHost(),
2183 self._GerritChangeIdentifier(),
2184 labels=labels,
2185 notify=notify)
2186 return 0
2187 except KeyboardInterrupt:
2188 raise
2189 except:
2190 print(
2191 'WARNING: Failed to %s.\n'
2192 'Either:\n'
2193 ' * Your project has no CQ,\n'
2194 ' * You don\'t have permission to change the CQ state,\n'
2195 ' * There\'s a bug in this code (see stack trace below).\n'
2196 'Consider specifying which bots to trigger manually or asking your '
2197 'project owners for permissions or contacting Chrome Infra at:\n'
2198 'https://www.chromium.org/infra\n\n' %
2199 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
2200 # Still raise exception so that stack trace is printed.
2201 raise
qyearsley1fdfcb62016-10-24 13:22:03 -07002202
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002203 def GetGerritHost(self):
2204 # Lazy load of configs.
2205 self.GetCodereviewServer()
Aravind Vasudevan0e3589e2023-05-25 23:00:30 +00002206
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002207 if self._gerrit_host and '.' not in self._gerrit_host:
2208 # Abbreviated domain like "chromium" instead of
2209 # chromium.googlesource.com.
2210 parsed = urllib.parse.urlparse(self.GetRemoteUrl())
2211 if parsed.scheme == 'sso':
2212 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2213 self._gerrit_server = 'https://%s' % self._gerrit_host
Aravind Vasudevan0e3589e2023-05-25 23:00:30 +00002214
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002215 return self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002216
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002217 def _GetGitHost(self):
2218 """Returns git host to be used when uploading change to Gerrit."""
2219 remote_url = self.GetRemoteUrl()
2220 if not remote_url:
2221 return None
2222 return urllib.parse.urlparse(remote_url).netloc
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002223
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002224 def GetCodereviewServer(self):
2225 if not self._gerrit_server:
2226 # If we're on a branch then get the server potentially associated
2227 # with that branch.
2228 if self.GetIssue() and self.GetBranch():
2229 self._gerrit_server = self._GitGetBranchConfigValue(
2230 CODEREVIEW_SERVER_CONFIG_KEY)
2231 if self._gerrit_server:
2232 self._gerrit_host = urllib.parse.urlparse(
2233 self._gerrit_server).netloc
2234 if not self._gerrit_server:
2235 url = urllib.parse.urlparse(self.GetRemoteUrl())
2236 parts = url.netloc.split('.')
Aravind Vasudevan0e3589e2023-05-25 23:00:30 +00002237
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002238 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2239 # has "-review" suffix for lowest level subdomain.
2240 parts[0] = parts[0] + '-review'
Aravind Vasudevan0e3589e2023-05-25 23:00:30 +00002241
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002242 if url.scheme == 'sso' and len(parts) == 1:
2243 # sso:// uses abbreivated hosts, eg. sso://chromium instead
2244 # of chromium.googlesource.com. Hence, for code review
2245 # server, they need to be expanded.
2246 parts[0] += '.googlesource.com'
Aravind Vasudevan0e3589e2023-05-25 23:00:30 +00002247
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002248 self._gerrit_host = '.'.join(parts)
2249 self._gerrit_server = 'https://%s' % self._gerrit_host
2250 return self._gerrit_server
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002251
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002252 def GetGerritProject(self):
2253 """Returns Gerrit project name based on remote git URL."""
2254 remote_url = self.GetRemoteUrl()
2255 if remote_url is None:
2256 logging.warning('can\'t detect Gerrit project.')
2257 return None
2258 project = urllib.parse.urlparse(remote_url).path.strip('/')
2259 if project.endswith('.git'):
2260 project = project[:-len('.git')]
2261 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start
2262 # with 'a/' prefix, because 'a/' prefix is used to force authentication
2263 # in gitiles/git-over-https protocol. E.g.,
2264 # https://chromium.googlesource.com/a/v8/v8 refers to the same
2265 # repo/project as https://chromium.googlesource.com/v8/v8
2266 if project.startswith('a/'):
2267 project = project[len('a/'):]
2268 return project
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002269
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002270 def _GerritChangeIdentifier(self):
2271 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002272
2273 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002274 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002275 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002276 project = self.GetGerritProject()
2277 if project:
2278 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
2279 # Fall back on still unique, but less efficient change number.
2280 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002281
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002282 def EnsureAuthenticated(self, force, refresh=None):
2283 """Best effort check that user is authenticated with Gerrit server."""
2284 if settings.GetGerritSkipEnsureAuthenticated():
2285 # For projects with unusual authentication schemes.
2286 # See http://crbug.com/603378.
2287 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002288
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002289 # Check presence of cookies only if using cookies-based auth method.
2290 cookie_auth = gerrit_util.Authenticator.get()
2291 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
2292 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002293
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002294 remote_url = self.GetRemoteUrl()
2295 if remote_url is None:
2296 logging.warning('invalid remote')
2297 return
2298 if urllib.parse.urlparse(remote_url).scheme not in ['https', 'sso']:
2299 logging.warning(
2300 'Ignoring branch %(branch)s with non-https/sso remote '
2301 '%(remote)s', {
2302 'branch': self.branch,
2303 'remote': self.GetRemoteUrl()
2304 })
2305 return
Daniel Chengcf6269b2019-05-18 01:02:12 +00002306
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002307 # Lazy-loader to identify Gerrit and Git hosts.
2308 self.GetCodereviewServer()
2309 git_host = self._GetGitHost()
2310 assert self._gerrit_server and self._gerrit_host and git_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002311
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002312 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2313 git_auth = cookie_auth.get_auth_header(git_host)
2314 if gerrit_auth and git_auth:
2315 if gerrit_auth == git_auth:
2316 return
2317 all_gsrc = cookie_auth.get_auth_header(
2318 'd0esN0tEx1st.googlesource.com')
2319 print(
2320 'WARNING: You have different credentials for Gerrit and git hosts:\n'
2321 ' %s\n'
2322 ' %s\n'
2323 ' Consider running the following command:\n'
2324 ' git cl creds-check\n'
2325 ' %s\n'
2326 ' %s' %
2327 (git_host, self._gerrit_host,
2328 ('Hint: delete creds for .googlesource.com' if all_gsrc else
2329 ''), cookie_auth.get_new_password_message(git_host)))
2330 if not force:
2331 confirm_or_exit('If you know what you are doing',
2332 action='continue')
2333 return
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00002334
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002335 missing = (([] if gerrit_auth else [self._gerrit_host]) +
2336 ([] if git_auth else [git_host]))
2337 DieWithError('Credentials for the following hosts are required:\n'
2338 ' %s\n'
2339 'These are read from %s (or legacy %s)\n'
2340 '%s' %
2341 ('\n '.join(missing), cookie_auth.get_gitcookies_path(),
2342 cookie_auth.get_netrc_path(),
2343 cookie_auth.get_new_password_message(git_host)))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002344
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002345 def EnsureCanUploadPatchset(self, force):
2346 if not self.GetIssue():
2347 return
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002348
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002349 status = self._GetChangeDetail()['status']
2350 if status == 'ABANDONED':
2351 DieWithError(
2352 'Change %s has been abandoned, new uploads are not allowed' %
2353 (self.GetIssueURL()))
2354 if status == 'MERGED':
2355 answer = gclient_utils.AskForData(
2356 'Change %s has been submitted, new uploads are not allowed. '
2357 'Would you like to start a new change (Y/n)?' %
2358 self.GetIssueURL()).lower()
2359 if answer not in ('y', ''):
2360 DieWithError('New uploads are not allowed.')
2361 self.SetIssue()
2362 return
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002363
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002364 # TODO(vadimsh): For some reason the chunk of code below was skipped if
2365 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
2366 # Apparently this check is not very important? Otherwise get_auth_email
2367 # could have been added to other implementations of Authenticator.
2368 cookies_auth = gerrit_util.Authenticator.get()
2369 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
2370 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002371
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002372 cookies_user = cookies_auth.get_auth_email(self.GetGerritHost())
2373 if self.GetIssueOwner() == cookies_user:
2374 return
2375 logging.debug('change %s owner is %s, cookies user is %s',
2376 self.GetIssue(), self.GetIssueOwner(), cookies_user)
2377 # Maybe user has linked accounts or something like that,
2378 # so ask what Gerrit thinks of this user.
2379 details = gerrit_util.GetAccountDetails(self.GetGerritHost(), 'self')
2380 if details['email'] == self.GetIssueOwner():
2381 return
2382 if not force:
2383 print(
2384 'WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
2385 'as %s.\n'
2386 'Uploading may fail due to lack of permissions.' %
2387 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2388 confirm_or_exit(action='upload')
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002389
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002390 def GetStatus(self):
2391 """Applies a rough heuristic to give a simple summary of an issue's review
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002392 or CQ status, assuming adherence to a common workflow.
2393
2394 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002395 * 'error' - error from review tool (including deleted issues)
2396 * 'unsent' - no reviewers added
2397 * 'waiting' - waiting for review
2398 * 'reply' - waiting for uploader to reply to review
2399 * 'lgtm' - Code-Review label has been set
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00002400 * 'dry-run' - dry-running in the CQ
2401 * 'commit' - in the CQ
Aaron Gable9ab38c62017-04-06 14:36:33 -07002402 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002403 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002404 if not self.GetIssue():
2405 return None
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002406
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002407 try:
2408 data = self._GetChangeDetail(
2409 ['DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
2410 except GerritChangeNotExists:
2411 return 'error'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002412
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002413 if data['status'] in ('ABANDONED', 'MERGED'):
2414 return 'closed'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002415
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002416 cq_label = data['labels'].get('Commit-Queue', {})
2417 max_cq_vote = 0
2418 for vote in cq_label.get('all', []):
2419 max_cq_vote = max(max_cq_vote, vote.get('value', 0))
2420 if max_cq_vote == 2:
2421 return 'commit'
2422 if max_cq_vote == 1:
2423 return 'dry-run'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002424
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002425 if data['labels'].get('Code-Review', {}).get('approved'):
2426 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002427
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002428 if not data.get('reviewers', {}).get('REVIEWER', []):
2429 return 'unsent'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002430
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002431 owner = data['owner'].get('_account_id')
2432 messages = sorted(data.get('messages', []), key=lambda m: m.get('date'))
2433 while messages:
2434 m = messages.pop()
2435 if (m.get('tag', '').startswith('autogenerated:cq')
2436 or m.get('tag', '').startswith('autogenerated:cv')):
2437 # Ignore replies from LUCI CV/CQ.
2438 continue
2439 if m.get('author', {}).get('_account_id') == owner:
2440 # Most recent message was by owner.
2441 return 'waiting'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00002442
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002443 # Some reply from non-owner.
2444 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002445
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002446 # Somehow there are no messages even though there are reviewers.
2447 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002448
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002449 def GetMostRecentPatchset(self, update=True):
2450 if not self.GetIssue():
2451 return None
Edward Lemur6c6827c2020-02-06 21:15:18 +00002452
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002453 data = self._GetChangeDetail(['CURRENT_REVISION'])
2454 patchset = data['revisions'][data['current_revision']]['_number']
2455 if update:
2456 self.SetPatchset(patchset)
2457 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002458
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002459 def _IsPatchsetRangeSignificant(self, lower, upper):
2460 """Returns True if the inclusive range of patchsets contains any reworks or
Gavin Makf35a9eb2022-11-17 18:34:36 +00002461 rebases."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002462 if not self.GetIssue():
2463 return False
Gavin Makf35a9eb2022-11-17 18:34:36 +00002464
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002465 data = self._GetChangeDetail(['ALL_REVISIONS'])
2466 ps_kind = {}
2467 for rev_info in data.get('revisions', {}).values():
2468 ps_kind[rev_info['_number']] = rev_info.get('kind', '')
Gavin Makf35a9eb2022-11-17 18:34:36 +00002469
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002470 for ps in range(lower, upper + 1):
2471 assert ps in ps_kind, 'expected patchset %d in change detail' % ps
2472 if ps_kind[ps] not in ('NO_CHANGE', 'NO_CODE_CHANGE'):
2473 return True
2474 return False
Gavin Makf35a9eb2022-11-17 18:34:36 +00002475
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002476 def GetMostRecentDryRunPatchset(self):
2477 """Get patchsets equivalent to the most recent patchset and return
Gavin Make61ccc52020-11-13 00:12:57 +00002478 the patchset with the latest dry run. If none have been dry run, return
2479 the latest patchset."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002480 if not self.GetIssue():
2481 return None
Gavin Make61ccc52020-11-13 00:12:57 +00002482
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002483 data = self._GetChangeDetail(['ALL_REVISIONS'])
2484 patchset = data['revisions'][data['current_revision']]['_number']
2485 dry_run = {
2486 int(m['_revision_number'])
2487 for m in data.get('messages', [])
2488 if m.get('tag', '').endswith('dry-run')
2489 }
Gavin Make61ccc52020-11-13 00:12:57 +00002490
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002491 for revision_info in sorted(data.get('revisions', {}).values(),
2492 key=lambda c: c['_number'],
2493 reverse=True):
2494 if revision_info['_number'] in dry_run:
2495 patchset = revision_info['_number']
2496 break
2497 if revision_info.get('kind', '') not in \
2498 ('NO_CHANGE', 'NO_CODE_CHANGE', 'TRIVIAL_REBASE'):
2499 break
2500 self.SetPatchset(patchset)
2501 return patchset
Gavin Make61ccc52020-11-13 00:12:57 +00002502
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002503 def AddComment(self, message, publish=None):
2504 gerrit_util.SetReview(self.GetGerritHost(),
2505 self._GerritChangeIdentifier(),
2506 msg=message,
2507 ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002508
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002509 def GetCommentsSummary(self, readable=True):
2510 # DETAILED_ACCOUNTS is to get emails in accounts.
2511 # CURRENT_REVISION is included to get the latest patchset so that
2512 # only the robot comments from the latest patchset can be shown.
2513 messages = self._GetChangeDetail(
2514 options=['MESSAGES', 'DETAILED_ACCOUNTS', 'CURRENT_REVISION']).get(
2515 'messages', [])
2516 file_comments = gerrit_util.GetChangeComments(
2517 self.GetGerritHost(), self._GerritChangeIdentifier())
2518 robot_file_comments = gerrit_util.GetChangeRobotComments(
2519 self.GetGerritHost(), self._GerritChangeIdentifier())
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002520
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002521 # Add the robot comments onto the list of comments, but only
2522 # keep those that are from the latest patchset.
2523 latest_patch_set = self.GetMostRecentPatchset()
2524 for path, robot_comments in robot_file_comments.items():
2525 line_comments = file_comments.setdefault(path, [])
2526 line_comments.extend([
2527 c for c in robot_comments if c['patch_set'] == latest_patch_set
2528 ])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002529
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002530 # Build dictionary of file comments for easy access and sorting later.
2531 # {author+date: {path: {patchset: {line: url+message}}}}
2532 comments = collections.defaultdict(lambda: collections.defaultdict(
2533 lambda: collections.defaultdict(dict)))
Andrii Shyshkalova3762a92020-11-25 10:20:42 +00002534
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002535 server = self.GetCodereviewServer()
2536 if server in _KNOWN_GERRIT_TO_SHORT_URLS:
2537 # /c/ is automatically added by short URL server.
2538 url_prefix = '%s/%s' % (_KNOWN_GERRIT_TO_SHORT_URLS[server],
2539 self.GetIssue())
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002540 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002541 url_prefix = '%s/c/%s' % (server, self.GetIssue())
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002542
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002543 for path, line_comments in file_comments.items():
2544 for comment in line_comments:
2545 tag = comment.get('tag', '')
2546 if tag.startswith(
2547 'autogenerated') and 'robot_id' not in comment:
2548 continue
2549 key = (comment['author']['email'], comment['updated'])
2550 if comment.get('side', 'REVISION') == 'PARENT':
2551 patchset = 'Base'
2552 else:
2553 patchset = 'PS%d' % comment['patch_set']
2554 line = comment.get('line', 0)
2555 url = ('%s/%s/%s#%s%s' %
2556 (url_prefix, comment['patch_set'],
2557 path, 'b' if comment.get('side') == 'PARENT' else '',
2558 str(line) if line else ''))
2559 comments[key][path][patchset][line] = (url, comment['message'])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002560
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002561 summaries = []
2562 for msg in messages:
2563 summary = self._BuildCommentSummary(msg, comments, readable)
2564 if summary:
2565 summaries.append(summary)
2566 return summaries
Josip Sokcevic266129c2021-11-09 00:22:00 +00002567
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002568 @staticmethod
2569 def _BuildCommentSummary(msg, comments, readable):
2570 if 'email' not in msg['author']:
2571 # Some bot accounts may not have an email associated.
2572 return None
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002573
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002574 key = (msg['author']['email'], msg['date'])
2575 # Don't bother showing autogenerated messages that don't have associated
2576 # file or line comments. this will filter out most autogenerated
2577 # messages, but will keep robot comments like those from Tricium.
2578 is_autogenerated = msg.get('tag', '').startswith('autogenerated')
2579 if is_autogenerated and not comments.get(key):
2580 return None
2581 message = msg['message']
2582 # Gerrit spits out nanoseconds.
2583 assert len(msg['date'].split('.')[-1]) == 9
2584 date = datetime.datetime.strptime(msg['date'][:-3],
2585 '%Y-%m-%d %H:%M:%S.%f')
2586 if key in comments:
2587 message += '\n'
2588 for path, patchsets in sorted(comments.get(key, {}).items()):
2589 if readable:
2590 message += '\n%s' % path
2591 for patchset, lines in sorted(patchsets.items()):
2592 for line, (url, content) in sorted(lines.items()):
2593 if line:
2594 line_str = 'Line %d' % line
2595 path_str = '%s:%d:' % (path, line)
2596 else:
2597 line_str = 'File comment'
2598 path_str = '%s:0:' % path
2599 if readable:
2600 message += '\n %s, %s: %s' % (patchset, line_str, url)
2601 message += '\n %s\n' % content
2602 else:
2603 message += '\n%s ' % path_str
2604 message += '\n%s\n' % content
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002605
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002606 return _CommentSummary(
2607 date=date,
2608 message=message,
2609 sender=msg['author']['email'],
2610 autogenerated=is_autogenerated,
2611 # These could be inferred from the text messages and correlated with
2612 # Code-Review label maximum, however this is not reliable.
2613 # Leaving as is until the need arises.
2614 approval=False,
2615 disapproval=False,
2616 )
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002617
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002618 def CloseIssue(self):
2619 gerrit_util.AbandonChange(self.GetGerritHost(),
2620 self._GerritChangeIdentifier(),
2621 msg='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002622
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002623 def SubmitIssue(self):
2624 gerrit_util.SubmitChange(self.GetGerritHost(),
2625 self._GerritChangeIdentifier())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002626
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002627 def _GetChangeDetail(self, options=None):
2628 """Returns details of associated Gerrit change and caching results."""
2629 options = options or []
2630 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002631
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002632 # Optimization to avoid multiple RPCs:
2633 if 'CURRENT_REVISION' in options or 'ALL_REVISIONS' in options:
2634 options.append('CURRENT_COMMIT')
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002635
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002636 # Normalize issue and options for consistent keys in cache.
2637 cache_key = str(self.GetIssue())
2638 options_set = frozenset(o.upper() for o in options)
2639
2640 for cached_options_set, data in self._detail_cache.get(cache_key, []):
2641 # Assumption: data fetched before with extra options is suitable
2642 # for return for a smaller set of options.
2643 # For example, if we cached data for
2644 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2645 # and request is for options=[CURRENT_REVISION],
2646 # THEN we can return prior cached data.
2647 if options_set.issubset(cached_options_set):
2648 return data
2649
2650 try:
2651 data = gerrit_util.GetChangeDetail(self.GetGerritHost(),
2652 self._GerritChangeIdentifier(),
2653 options_set)
2654 except gerrit_util.GerritError as e:
2655 if e.http_status == 404:
2656 raise GerritChangeNotExists(self.GetIssue(),
2657 self.GetCodereviewServer())
2658 raise
2659
2660 self._detail_cache.setdefault(cache_key, []).append((options_set, data))
Edward Lesmes7677e5c2020-02-19 20:39:03 +00002661 return data
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002662
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002663 def _GetChangeCommit(self, revision='current'):
2664 assert self.GetIssue(), 'issue must be set to query Gerrit'
2665 try:
2666 data = gerrit_util.GetChangeCommit(self.GetGerritHost(),
2667 self._GerritChangeIdentifier(),
2668 revision)
2669 except gerrit_util.GerritError as e:
2670 if e.http_status == 404:
2671 raise GerritChangeNotExists(self.GetIssue(),
2672 self.GetCodereviewServer())
2673 raise
2674 return data
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002675
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002676 def _IsCqConfigured(self):
2677 detail = self._GetChangeDetail(['LABELS'])
2678 return u'Commit-Queue' in detail.get('labels', {})
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002679
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002680 def CMDLand(self, force, bypass_hooks, verbose, parallel, resultdb, realm):
2681 if git_common.is_dirty_git_tree('land'):
2682 return 1
agable32978d92016-11-01 12:55:02 -07002683
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002684 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2685 if not force and self._IsCqConfigured():
2686 confirm_or_exit(
2687 '\nIt seems this repository has a CQ, '
2688 'which can test and land changes for you. '
2689 'Are you sure you wish to bypass it?\n',
2690 action='bypass CQ')
2691 differs = True
2692 last_upload = self._GitGetBranchConfigValue(
Gavin Mak4e5e3992022-11-14 22:40:12 +00002693 GERRIT_SQUASH_HASH_CONFIG_KEY)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002694 # Note: git diff outputs nothing if there is no diff.
2695 if not last_upload or RunGit(['diff', last_upload]).strip():
2696 print(
2697 'WARNING: Some changes from local branch haven\'t been uploaded.'
2698 )
Edward Lemur5a644f82020-03-18 16:44:57 +00002699 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002700 if detail['current_revision'] == last_upload:
2701 differs = False
2702 else:
2703 print(
2704 'WARNING: Local branch contents differ from latest uploaded '
2705 'patchset.')
2706 if differs:
2707 if not force:
2708 confirm_or_exit(
2709 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2710 action='submit')
2711 print(
2712 'WARNING: Bypassing hooks and submitting latest uploaded patchset.'
2713 )
2714 elif not bypass_hooks:
2715 upstream = self.GetCommonAncestorWithUpstream()
2716 if self.GetIssue():
2717 description = self.FetchDescription()
2718 else:
2719 description = _create_description_from_log([upstream])
2720 self.RunHook(committing=True,
2721 may_prompt=not force,
2722 verbose=verbose,
2723 parallel=parallel,
2724 upstream=upstream,
2725 description=description,
2726 all_files=False,
2727 resultdb=resultdb,
2728 realm=realm)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002729
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002730 self.SubmitIssue()
2731 print('Issue %s has been submitted.' % self.GetIssueURL())
2732 links = self._GetChangeCommit().get('web_links', [])
2733 for link in links:
2734 if link.get('name') in ['gitiles', 'browse'] and link.get('url'):
2735 print('Landed as: %s' % link.get('url'))
2736 break
2737 return 0
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002738
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002739 def CMDPatchWithParsedIssue(self, parsed_issue_arg, nocommit, force,
2740 newbranch):
2741 assert parsed_issue_arg.valid
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002742
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002743 self.issue = parsed_issue_arg.issue
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002744
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002745 if parsed_issue_arg.hostname:
2746 self._gerrit_host = parsed_issue_arg.hostname
2747 self._gerrit_server = 'https://%s' % self._gerrit_host
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002748
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002749 try:
2750 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2751 except GerritChangeNotExists as e:
2752 DieWithError(str(e))
agablec6787972016-09-09 16:13:34 -07002753
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002754 if not parsed_issue_arg.patchset:
2755 # Use current revision by default.
2756 revision_info = detail['revisions'][detail['current_revision']]
2757 patchset = int(revision_info['_number'])
2758 else:
2759 patchset = parsed_issue_arg.patchset
2760 for revision_info in detail['revisions'].values():
2761 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2762 break
2763 else:
2764 DieWithError('Couldn\'t find patchset %i in change %i' %
2765 (parsed_issue_arg.patchset, self.GetIssue()))
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002766
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002767 remote_url = self.GetRemoteUrl()
2768 if remote_url.endswith('.git'):
2769 remote_url = remote_url[:-len('.git')]
2770 remote_url = remote_url.rstrip('/')
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002771
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002772 fetch_info = revision_info['fetch']['http']
2773 fetch_info['url'] = fetch_info['url'].rstrip('/')
Josip Sokcevicf2cfd3d2021-03-30 18:39:18 +00002774
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002775 if remote_url != fetch_info['url']:
2776 DieWithError(
2777 'Trying to patch a change from %s but this repo appears '
2778 'to be %s.' % (fetch_info['url'], remote_url))
Gavin Mak4e5e3992022-11-14 22:40:12 +00002779
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002780 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002781
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002782 # Set issue immediately in case the cherry-pick fails, which happens
2783 # when resolving conflicts.
2784 if self.GetBranch():
2785 self.SetIssue(parsed_issue_arg.issue)
tandrii88189772016-09-29 04:29:57 -07002786
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002787 if force:
2788 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2789 print('Checked out commit for change %i patchset %i locally' %
2790 (parsed_issue_arg.issue, patchset))
2791 elif nocommit:
2792 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2793 print('Patch applied to index.')
2794 else:
2795 RunGit(['cherry-pick', 'FETCH_HEAD'])
2796 print('Committed patch for change %i patchset %i locally.' %
2797 (parsed_issue_arg.issue, patchset))
2798 print(
2799 'Note: this created a local commit which does not have '
2800 'the same hash as the one uploaded for review. This will make '
2801 'uploading changes based on top of this branch difficult.\n'
2802 'If you want to do that, use "git cl patch --force" instead.')
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002803
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002804 if self.GetBranch():
2805 self.SetPatchset(patchset)
2806 fetched_hash = scm.GIT.ResolveCommit(settings.GetRoot(),
2807 'FETCH_HEAD')
2808 self._GitSetBranchConfigValue(LAST_UPLOAD_HASH_CONFIG_KEY,
2809 fetched_hash)
2810 self._GitSetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY,
2811 fetched_hash)
2812 else:
2813 print(
2814 'WARNING: You are in detached HEAD state.\n'
2815 'The patch has been applied to your checkout, but you will not be '
2816 'able to upload a new patch set to the gerrit issue.\n'
2817 'Try using the \'-b\' option if you would like to work on a '
2818 'branch and/or upload a new patch set.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002819
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002820 return 0
2821
2822 @staticmethod
2823 def _GerritCommitMsgHookCheck(offer_removal):
2824 # type: (bool) -> None
2825 """Checks for the gerrit's commit-msg hook and removes it if necessary."""
2826 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2827 if not os.path.exists(hook):
2828 return
2829 # Crude attempt to distinguish Gerrit Codereview hook from a potentially
2830 # custom developer-made one.
2831 data = gclient_utils.FileRead(hook)
2832 if not ('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2833 return
2834 print('WARNING: You have Gerrit commit-msg hook installed.\n'
2835 'It is not necessary for uploading with git cl in squash mode, '
2836 'and may interfere with it in subtle ways.\n'
2837 'We recommend you remove the commit-msg hook.')
2838 if offer_removal:
2839 if ask_for_explicit_yes('Do you want to remove it now?'):
2840 gclient_utils.rm_file_or_tree(hook)
2841 print('Gerrit commit-msg hook removed.')
2842 else:
2843 print('OK, will keep Gerrit commit-msg hook in place.')
2844
2845 def _CleanUpOldTraces(self):
2846 """Keep only the last |MAX_TRACES| traces."""
2847 try:
2848 traces = sorted([
2849 os.path.join(TRACES_DIR, f) for f in os.listdir(TRACES_DIR)
2850 if (os.path.isfile(os.path.join(TRACES_DIR, f))
2851 and not f.startswith('tmp'))
2852 ])
2853 traces_to_delete = traces[:-MAX_TRACES]
2854 for trace in traces_to_delete:
2855 os.remove(trace)
2856 except OSError:
2857 print('WARNING: Failed to remove old git traces from\n'
2858 ' %s'
2859 'Consider removing them manually.' % TRACES_DIR)
2860
2861 def _WriteGitPushTraces(self, trace_name, traces_dir, git_push_metadata):
2862 """Zip and write the git push traces stored in traces_dir."""
2863 gclient_utils.safe_makedirs(TRACES_DIR)
2864 traces_zip = trace_name + '-traces'
2865 traces_readme = trace_name + '-README'
2866 # Create a temporary dir to store git config and gitcookies in. It will
2867 # be compressed and stored next to the traces.
2868 git_info_dir = tempfile.mkdtemp()
2869 git_info_zip = trace_name + '-git-info'
2870
2871 git_push_metadata['now'] = datetime_now().strftime(
2872 '%Y-%m-%dT%H:%M:%S.%f')
2873
2874 git_push_metadata['trace_name'] = trace_name
2875 gclient_utils.FileWrite(traces_readme,
2876 TRACES_README_FORMAT % git_push_metadata)
2877
2878 # Keep only the first 6 characters of the git hashes on the packet
2879 # trace. This greatly decreases size after compression.
2880 packet_traces = os.path.join(traces_dir, 'trace-packet')
2881 if os.path.isfile(packet_traces):
2882 contents = gclient_utils.FileRead(packet_traces)
2883 gclient_utils.FileWrite(packet_traces,
2884 GIT_HASH_RE.sub(r'\1', contents))
2885 shutil.make_archive(traces_zip, 'zip', traces_dir)
2886
2887 # Collect and compress the git config and gitcookies.
2888 git_config = RunGit(['config', '-l'])
2889 gclient_utils.FileWrite(os.path.join(git_info_dir, 'git-config'),
2890 git_config)
2891
2892 cookie_auth = gerrit_util.Authenticator.get()
2893 if isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
2894 gitcookies_path = cookie_auth.get_gitcookies_path()
2895 if os.path.isfile(gitcookies_path):
2896 gitcookies = gclient_utils.FileRead(gitcookies_path)
2897 gclient_utils.FileWrite(
2898 os.path.join(git_info_dir, 'gitcookies'),
2899 GITCOOKIES_REDACT_RE.sub('REDACTED', gitcookies))
2900 shutil.make_archive(git_info_zip, 'zip', git_info_dir)
2901
2902 gclient_utils.rmtree(git_info_dir)
2903
2904 def _RunGitPushWithTraces(self,
2905 refspec,
2906 refspec_opts,
2907 git_push_metadata,
2908 git_push_options=None):
2909 """Run git push and collect the traces resulting from the execution."""
2910 # Create a temporary directory to store traces in. Traces will be
2911 # compressed and stored in a 'traces' dir inside depot_tools.
2912 traces_dir = tempfile.mkdtemp()
2913 trace_name = os.path.join(TRACES_DIR,
2914 datetime_now().strftime('%Y%m%dT%H%M%S.%f'))
2915
2916 env = os.environ.copy()
2917 env['GIT_REDACT_COOKIES'] = 'o,SSO,GSSO_Uberproxy'
2918 env['GIT_TR2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
2919 env['GIT_TRACE2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
2920 env['GIT_TRACE_CURL'] = os.path.join(traces_dir, 'trace-curl')
2921 env['GIT_TRACE_CURL_NO_DATA'] = '1'
2922 env['GIT_TRACE_PACKET'] = os.path.join(traces_dir, 'trace-packet')
2923
2924 push_returncode = 0
2925 before_push = time_time()
2926 try:
2927 remote_url = self.GetRemoteUrl()
2928 push_cmd = ['git', 'push', remote_url, refspec]
2929 if git_push_options:
2930 for opt in git_push_options:
2931 push_cmd.extend(['-o', opt])
2932
2933 push_stdout = gclient_utils.CheckCallAndFilter(
2934 push_cmd,
2935 env=env,
2936 print_stdout=True,
2937 # Flush after every line: useful for seeing progress when
2938 # running as recipe.
2939 filter_fn=lambda _: sys.stdout.flush())
2940 push_stdout = push_stdout.decode('utf-8', 'replace')
2941 except subprocess2.CalledProcessError as e:
2942 push_returncode = e.returncode
2943 if 'blocked keyword' in str(e.stdout) or 'banned word' in str(
2944 e.stdout):
2945 raise GitPushError(
2946 'Failed to create a change, very likely due to blocked keyword. '
2947 'Please examine output above for the reason of the failure.\n'
2948 'If this is a false positive, you can try to bypass blocked '
2949 'keyword by using push option '
2950 '-o banned-words~skip, e.g.:\n'
2951 'git cl upload -o banned-words~skip\n\n'
2952 'If git-cl is not working correctly, file a bug under the '
2953 'Infra>SDK component.')
2954 if 'git push -o nokeycheck' in str(e.stdout):
2955 raise GitPushError(
2956 'Failed to create a change, very likely due to a private key being '
2957 'detected. Please examine output above for the reason of the '
2958 'failure.\n'
2959 'If this is a false positive, you can try to bypass private key '
2960 'detection by using push option '
2961 '-o nokeycheck, e.g.:\n'
2962 'git cl upload -o nokeycheck\n\n'
2963 'If git-cl is not working correctly, file a bug under the '
2964 'Infra>SDK component.')
2965
2966 raise GitPushError(
2967 'Failed to create a change. Please examine output above for the '
2968 'reason of the failure.\n'
2969 'For emergencies, Googlers can escalate to '
2970 'go/gob-support or go/notify#gob\n'
2971 'Hint: run command below to diagnose common Git/Gerrit '
2972 'credential problems:\n'
2973 ' git cl creds-check\n'
2974 '\n'
2975 'If git-cl is not working correctly, file a bug under the Infra>SDK '
2976 'component including the files below.\n'
2977 'Review the files before upload, since they might contain sensitive '
2978 'information.\n'
2979 'Set the Restrict-View-Google label so that they are not publicly '
2980 'accessible.\n' + TRACES_MESSAGE % {'trace_name': trace_name})
2981 finally:
2982 execution_time = time_time() - before_push
2983 metrics.collector.add_repeated(
2984 'sub_commands', {
2985 'command':
2986 'git push',
2987 'execution_time':
2988 execution_time,
2989 'exit_code':
2990 push_returncode,
2991 'arguments':
2992 metrics_utils.extract_known_subcommand_args(refspec_opts),
2993 })
2994
2995 git_push_metadata['execution_time'] = execution_time
2996 git_push_metadata['exit_code'] = push_returncode
2997 self._WriteGitPushTraces(trace_name, traces_dir, git_push_metadata)
2998
2999 self._CleanUpOldTraces()
3000 gclient_utils.rmtree(traces_dir)
3001
3002 return push_stdout
3003
3004 def CMDUploadChange(self, options, git_diff_args, custom_cl_base,
3005 change_desc):
3006 """Upload the current branch to Gerrit, retry if new remote HEAD is
3007 found. options and change_desc may be mutated."""
3008 remote, remote_branch = self.GetRemoteBranch()
3009 branch = GetTargetRef(remote, remote_branch, options.target_branch)
3010
3011 try:
3012 return self._CMDUploadChange(options, git_diff_args, custom_cl_base,
3013 change_desc, branch)
3014 except GitPushError as e:
3015 # Repository might be in the middle of transition to main branch as
3016 # default, and uploads to old default might be blocked.
3017 if remote_branch not in [DEFAULT_OLD_BRANCH, DEFAULT_NEW_BRANCH]:
3018 DieWithError(str(e), change_desc)
3019
3020 project_head = gerrit_util.GetProjectHead(self._gerrit_host,
3021 self.GetGerritProject())
3022 if project_head == branch:
3023 DieWithError(str(e), change_desc)
3024 branch = project_head
3025
3026 print("WARNING: Fetching remote state and retrying upload to default "
3027 "branch...")
3028 RunGit(['fetch', '--prune', remote])
3029 options.edit_description = False
3030 options.force = True
3031 try:
3032 self._CMDUploadChange(options, git_diff_args, custom_cl_base,
3033 change_desc, branch)
3034 except GitPushError as e:
3035 DieWithError(str(e), change_desc)
3036
3037 def _CMDUploadChange(self, options, git_diff_args, custom_cl_base,
3038 change_desc, branch):
3039 """Upload the current branch to Gerrit."""
3040 if options.squash:
3041 Changelist._GerritCommitMsgHookCheck(
3042 offer_removal=not options.force)
3043 external_parent = None
3044 if self.GetIssue():
3045 # User requested to change description
3046 if options.edit_description:
3047 change_desc.prompt()
3048 change_detail = self._GetChangeDetail(['CURRENT_REVISION'])
3049 change_id = change_detail['change_id']
3050 change_desc.ensure_change_id(change_id)
3051
3052 # Check if changes outside of this workspace have been uploaded.
3053 current_rev = change_detail['current_revision']
3054 last_uploaded_rev = self._GitGetBranchConfigValue(
3055 GERRIT_SQUASH_HASH_CONFIG_KEY)
3056 if last_uploaded_rev and current_rev != last_uploaded_rev:
3057 external_parent = self._UpdateWithExternalChanges()
3058 else: # if not self.GetIssue()
3059 if not options.force and not options.message_file:
3060 change_desc.prompt()
3061 change_ids = git_footers.get_footer_change_id(
3062 change_desc.description)
3063 if len(change_ids) == 1:
3064 change_id = change_ids[0]
3065 else:
3066 change_id = GenerateGerritChangeId(change_desc.description)
3067 change_desc.ensure_change_id(change_id)
3068
3069 if options.preserve_tryjobs:
3070 change_desc.set_preserve_tryjobs()
3071
3072 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
3073 parent = external_parent or self._ComputeParent(
3074 remote, upstream_branch, custom_cl_base, options.force,
3075 change_desc)
3076 tree = RunGit(['rev-parse', 'HEAD:']).strip()
3077 with gclient_utils.temporary_file() as desc_tempfile:
3078 gclient_utils.FileWrite(desc_tempfile, change_desc.description)
3079 ref_to_push = RunGit(
3080 ['commit-tree', tree, '-p', parent, '-F',
3081 desc_tempfile]).strip()
3082 else: # if not options.squash
3083 if options.no_add_changeid:
3084 pass
3085 else: # adding Change-Ids is okay.
3086 if not git_footers.get_footer_change_id(
3087 change_desc.description):
3088 DownloadGerritHook(False)
3089 change_desc.set_description(
3090 self._AddChangeIdToCommitMessage(
3091 change_desc.description, git_diff_args))
3092 ref_to_push = 'HEAD'
3093 # For no-squash mode, we assume the remote called "origin" is the
3094 # one we want. It is not worthwhile to support different workflows
3095 # for no-squash mode.
3096 parent = 'origin/%s' % branch
3097 # attempt to extract the changeid from the current description
3098 # fail informatively if not possible.
3099 change_id_candidates = git_footers.get_footer_change_id(
3100 change_desc.description)
3101 if not change_id_candidates:
3102 DieWithError("Unable to extract change-id from message.")
3103 change_id = change_id_candidates[0]
3104
3105 SaveDescriptionBackup(change_desc)
3106 commits = RunGitSilent(['rev-list',
3107 '%s..%s' % (parent, ref_to_push)]).splitlines()
3108 if len(commits) > 1:
3109 print(
3110 'WARNING: This will upload %d commits. Run the following command '
3111 'to see which commits will be uploaded: ' % len(commits))
3112 print('git log %s..%s' % (parent, ref_to_push))
3113 print('You can also use `git squash-branch` to squash these into a '
3114 'single commit.')
3115 confirm_or_exit(action='upload')
3116
3117 reviewers = sorted(change_desc.get_reviewers())
3118 cc = []
3119 # Add default, watchlist, presubmit ccs if this is the initial upload
3120 # and CL is not private and auto-ccing has not been disabled.
3121 if not options.private and not options.no_autocc and not self.GetIssue(
3122 ):
3123 cc = self.GetCCList().split(',')
3124 if len(cc) > 100:
3125 lsc = ('https://chromium.googlesource.com/chromium/src/+/HEAD/docs/'
3126 'process/lsc/lsc_workflow.md')
3127 print('WARNING: This will auto-CC %s users.' % len(cc))
3128 print('LSC may be more appropriate: %s' % lsc)
3129 print('You can also use the --no-autocc flag to disable auto-CC.')
3130 confirm_or_exit(action='continue')
3131 # Add cc's from the --cc flag.
3132 if options.cc:
3133 cc.extend(options.cc)
3134 cc = [email.strip() for email in cc if email.strip()]
3135 if change_desc.get_cced():
3136 cc.extend(change_desc.get_cced())
3137 if self.GetGerritHost() == 'chromium-review.googlesource.com':
3138 valid_accounts = set(reviewers + cc)
3139 # TODO(crbug/877717): relax this for all hosts.
3140 else:
3141 valid_accounts = gerrit_util.ValidAccounts(self.GetGerritHost(),
3142 reviewers + cc)
3143 logging.info('accounts %s are recognized, %s invalid',
3144 sorted(valid_accounts),
3145 set(reviewers + cc).difference(set(valid_accounts)))
3146
3147 # Extra options that can be specified at push time. Doc:
3148 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
3149 refspec_opts = self._GetRefSpecOptions(options, change_desc)
3150
3151 for r in sorted(reviewers):
3152 if r in valid_accounts:
3153 refspec_opts.append('r=%s' % r)
3154 reviewers.remove(r)
3155 else:
3156 # TODO(tandrii): this should probably be a hard failure.
3157 print(
3158 'WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
3159 % r)
3160 for c in sorted(cc):
3161 # refspec option will be rejected if cc doesn't correspond to an
3162 # account, even though REST call to add such arbitrary cc may
3163 # succeed.
3164 if c in valid_accounts:
3165 refspec_opts.append('cc=%s' % c)
3166 cc.remove(c)
3167
3168 refspec_suffix = ''
3169 if refspec_opts:
3170 refspec_suffix = '%' + ','.join(refspec_opts)
3171 assert ' ' not in refspec_suffix, (
3172 'spaces not allowed in refspec: "%s"' % refspec_suffix)
3173 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
3174
3175 git_push_metadata = {
3176 'gerrit_host': self.GetGerritHost(),
3177 'title': options.title or '<untitled>',
3178 'change_id': change_id,
3179 'description': change_desc.description,
3180 }
3181
3182 # Gerrit may or may not update fast enough to return the correct
3183 # patchset number after we push. Get the pre-upload patchset and
3184 # increment later.
3185 latest_ps = self.GetMostRecentPatchset(update=False) or 0
3186
3187 push_stdout = self._RunGitPushWithTraces(refspec, refspec_opts,
3188 git_push_metadata,
3189 options.push_options)
3190
3191 if options.squash:
3192 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
3193 change_numbers = [
3194 m.group(1) for m in map(regex.match, push_stdout.splitlines())
3195 if m
3196 ]
3197 if len(change_numbers) != 1:
3198 DieWithError((
3199 'Created|Updated %d issues on Gerrit, but only 1 expected.\n'
3200 'Change-Id: %s') % (len(change_numbers), change_id),
3201 change_desc)
3202 self.SetIssue(change_numbers[0])
3203 self.SetPatchset(latest_ps + 1)
3204 self._GitSetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY,
3205 ref_to_push)
3206
3207 if self.GetIssue() and (reviewers or cc):
3208 # GetIssue() is not set in case of non-squash uploads according to
3209 # tests. TODO(crbug.com/751901): non-squash uploads in git cl should
3210 # be removed.
3211 gerrit_util.AddReviewers(self.GetGerritHost(),
3212 self._GerritChangeIdentifier(),
3213 reviewers,
3214 cc,
3215 notify=bool(options.send_mail))
3216
3217 return 0
3218
3219 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
3220 change_desc):
3221 """Computes parent of the generated commit to be uploaded to Gerrit.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003222
3223 Returns revision or a ref name.
3224 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003225 if custom_cl_base:
3226 # Try to avoid creating additional unintended CLs when uploading,
3227 # unless user wants to take this risk.
3228 local_ref_of_target_remote = self.GetRemoteBranch()[1]
3229 code, _ = RunGitWithCode([
3230 'merge-base', '--is-ancestor', custom_cl_base,
3231 local_ref_of_target_remote
3232 ])
3233 if code == 1:
3234 print(
3235 '\nWARNING: Manually specified base of this CL `%s` '
3236 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
3237 'If you proceed with upload, more than 1 CL may be created by '
3238 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
3239 'If you are certain that specified base `%s` has already been '
3240 'uploaded to Gerrit as another CL, you may proceed.\n' %
3241 (custom_cl_base, local_ref_of_target_remote,
3242 custom_cl_base))
3243 if not force:
3244 confirm_or_exit(
3245 'Do you take responsibility for cleaning up potential mess '
3246 'resulting from proceeding with upload?',
3247 action='upload')
3248 return custom_cl_base
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003249
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003250 if remote != '.':
3251 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003252
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003253 # If our upstream branch is local, we base our squashed commit on its
3254 # squashed version.
3255 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
Aaron Gablef97e33d2017-03-30 15:44:27 -07003256
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003257 if upstream_branch_name == 'master':
3258 return self.GetCommonAncestorWithUpstream()
3259 if upstream_branch_name == 'main':
3260 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003261
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003262 # Check the squashed hash of the parent.
3263 # TODO(tandrii): consider checking parent change in Gerrit and using its
3264 # hash if tree hash of latest parent revision (patchset) in Gerrit
3265 # matches the tree hash of the parent branch. The upside is less likely
3266 # bogus requests to reupload parent change just because it's uploadhash
3267 # is missing, yet the downside likely exists, too (albeit unknown to me
3268 # yet).
3269 parent = scm.GIT.GetBranchConfig(settings.GetRoot(),
3270 upstream_branch_name,
3271 GERRIT_SQUASH_HASH_CONFIG_KEY)
3272 # Verify that the upstream branch has been uploaded too, otherwise
3273 # Gerrit will create additional CLs when uploading.
3274 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
3275 RunGitSilent(['rev-parse', parent + ':'])):
3276 DieWithError(
3277 '\nUpload upstream branch %s first.\n'
3278 'It is likely that this branch has been rebased since its last '
3279 'upload, so you just need to upload it again.\n'
3280 '(If you uploaded it with --no-squash, then branch dependencies '
3281 'are not supported, and you should reupload with --squash.)' %
3282 upstream_branch_name, change_desc)
3283 return parent
Aaron Gablef97e33d2017-03-30 15:44:27 -07003284
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003285 def _UpdateWithExternalChanges(self):
3286 """Updates workspace with external changes.
Gavin Mak4e5e3992022-11-14 22:40:12 +00003287
3288 Returns the commit hash that should be used as the merge base on upload.
3289 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003290 local_ps = self.GetPatchset()
3291 if local_ps is None:
3292 return
Gavin Mak4e5e3992022-11-14 22:40:12 +00003293
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003294 external_ps = self.GetMostRecentPatchset(update=False)
3295 if external_ps is None or local_ps == external_ps or \
3296 not self._IsPatchsetRangeSignificant(local_ps + 1, external_ps):
3297 return
Gavin Mak4e5e3992022-11-14 22:40:12 +00003298
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003299 num_changes = external_ps - local_ps
3300 if num_changes > 1:
3301 change_words = 'changes were'
3302 else:
3303 change_words = 'change was'
3304 print('\n%d external %s published to %s:\n' %
3305 (num_changes, change_words, self.GetIssueURL(short=True)))
Gavin Mak6f905472023-01-06 21:01:36 +00003306
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003307 # Print an overview of external changes.
3308 ps_to_commit = {}
3309 ps_to_info = {}
3310 revisions = self._GetChangeDetail(['ALL_REVISIONS'])
3311 for commit_id, revision_info in revisions.get('revisions', {}).items():
3312 ps_num = revision_info['_number']
3313 ps_to_commit[ps_num] = commit_id
3314 ps_to_info[ps_num] = revision_info
Gavin Mak6f905472023-01-06 21:01:36 +00003315
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003316 for ps in range(external_ps, local_ps, -1):
3317 commit = ps_to_commit[ps][:8]
3318 desc = ps_to_info[ps].get('description', '')
3319 print('Patchset %d [%s] %s' % (ps, commit, desc))
Gavin Mak6f905472023-01-06 21:01:36 +00003320
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003321 print('\nSee diff at: %s/%d..%d' %
3322 (self.GetIssueURL(short=True), local_ps, external_ps))
3323 print('\nUploading without applying patches will override them.')
Josip Sokcevic43ceaf02023-05-25 15:56:00 +00003324
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003325 if not ask_for_explicit_yes('Get the latest changes and apply on top?'):
3326 return
Gavin Mak4e5e3992022-11-14 22:40:12 +00003327
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003328 # Get latest Gerrit merge base. Use the first parent even if multiple
3329 # exist.
3330 external_parent = self._GetChangeCommit(
3331 revision=external_ps)['parents'][0]
3332 external_base = external_parent['commit']
Gavin Mak4e5e3992022-11-14 22:40:12 +00003333
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003334 branch = git_common.current_branch()
3335 local_base = self.GetCommonAncestorWithUpstream()
3336 if local_base != external_base:
3337 print('\nLocal merge base %s is different from Gerrit %s.\n' %
3338 (local_base, external_base))
3339 if git_common.upstream(branch):
3340 confirm_or_exit(
3341 'Can\'t apply the latest changes from Gerrit.\n'
3342 'Continue with upload and override the latest changes?')
3343 return
3344 print(
3345 'No upstream branch set. Continuing upload with Gerrit merge base.'
3346 )
Gavin Mak4e5e3992022-11-14 22:40:12 +00003347
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003348 external_parent_last_uploaded = self._GetChangeCommit(
3349 revision=local_ps)['parents'][0]
3350 external_base_last_uploaded = external_parent_last_uploaded['commit']
Josip Sokcevic2568d4c2023-05-24 03:37:42 +00003351
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003352 if external_base != external_base_last_uploaded:
3353 print('\nPatch set merge bases are different (%s, %s).\n' %
3354 (external_base_last_uploaded, external_base))
3355 confirm_or_exit(
3356 'Can\'t apply the latest changes from Gerrit.\n'
3357 'Continue with upload and override the latest changes?')
3358 return
Josip Sokcevic2568d4c2023-05-24 03:37:42 +00003359
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003360 # Fetch Gerrit's CL base if it doesn't exist locally.
3361 remote, _ = self.GetRemoteBranch()
3362 if not scm.GIT.IsValidRevision(settings.GetRoot(), external_base):
3363 RunGitSilent(['fetch', remote, external_base])
Gavin Mak4e5e3992022-11-14 22:40:12 +00003364
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003365 # Get the diff between local_ps and external_ps.
3366 print('Fetching changes...')
3367 issue = self.GetIssue()
3368 changes_ref = 'refs/changes/%02d/%d/' % (issue % 100, issue)
3369 RunGitSilent(['fetch', remote, changes_ref + str(local_ps)])
3370 last_uploaded = RunGitSilent(['rev-parse', 'FETCH_HEAD']).strip()
3371 RunGitSilent(['fetch', remote, changes_ref + str(external_ps)])
3372 latest_external = RunGitSilent(['rev-parse', 'FETCH_HEAD']).strip()
Josip Sokcevic2568d4c2023-05-24 03:37:42 +00003373
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003374 # If the commit parents are different, don't apply the diff as it very
3375 # likely contains many more changes not relevant to this CL.
3376 parents = RunGitSilent(
3377 ['rev-parse',
3378 '%s~1' % (last_uploaded),
3379 '%s~1' % (latest_external)]).strip().split()
3380 assert len(parents) == 2, 'Expected two parents.'
3381 if parents[0] != parents[1]:
3382 confirm_or_exit(
3383 'Can\'t apply the latest changes from Gerrit (parent mismatch '
3384 'between PS).\n'
3385 'Continue with upload and override the latest changes?')
3386 return
Josip Sokcevic2568d4c2023-05-24 03:37:42 +00003387
Joanna Wangbcba1782023-09-12 22:48:05 +00003388 diff = RunGitSilent([
3389 'diff', '--no-ext-diff',
3390 '%s..%s' % (last_uploaded, latest_external)
3391 ])
Gavin Mak4e5e3992022-11-14 22:40:12 +00003392
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003393 # Diff can be empty in the case of trivial rebases.
3394 if not diff:
3395 return external_base
Gavin Mak4e5e3992022-11-14 22:40:12 +00003396
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003397 # Apply the diff.
3398 with gclient_utils.temporary_file() as diff_tempfile:
3399 gclient_utils.FileWrite(diff_tempfile, diff)
3400 clean_patch = RunGitWithCode(['apply', '--check',
3401 diff_tempfile])[0] == 0
3402 RunGitSilent(['apply', '-3', '--intent-to-add', diff_tempfile])
3403 if not clean_patch:
3404 # Normally patchset is set after upload. But because we exit,
3405 # that never happens. Updating here makes sure that subsequent
3406 # uploads don't need to fetch/apply the same diff again.
3407 self.SetPatchset(external_ps)
3408 DieWithError(
3409 '\nPatch did not apply cleanly. Please resolve any '
3410 'conflicts and reupload.')
Gavin Mak4e5e3992022-11-14 22:40:12 +00003411
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003412 message = 'Incorporate external changes from '
3413 if num_changes == 1:
3414 message += 'patchset %d' % external_ps
3415 else:
3416 message += 'patchsets %d to %d' % (local_ps + 1, external_ps)
3417 RunGitSilent(['commit', '-am', message])
3418 # TODO(crbug.com/1382528): Use the previous commit's message as a
3419 # default patchset title instead of this 'Incorporate' message.
3420 return external_base
Gavin Mak4e5e3992022-11-14 22:40:12 +00003421
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003422 def _AddChangeIdToCommitMessage(self, log_desc, args):
3423 """Re-commits using the current message, assumes the commit hook is in
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003424 place.
3425 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003426 RunGit(['commit', '--amend', '-m', log_desc])
3427 new_log_desc = _create_description_from_log(args)
3428 if git_footers.get_footer_change_id(new_log_desc):
3429 print('git-cl: Added Change-Id to commit message.')
3430 return new_log_desc
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00003431
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003432 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003433
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003434 def CannotTriggerTryJobReason(self):
3435 try:
3436 data = self._GetChangeDetail()
3437 except GerritChangeNotExists:
3438 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003439
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003440 if data['status'] in ('ABANDONED', 'MERGED'):
3441 return 'CL %s is closed' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003442
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003443 def GetGerritChange(self, patchset=None):
3444 """Returns a buildbucket.v2.GerritChange message for the current issue."""
3445 host = urllib.parse.urlparse(self.GetCodereviewServer()).hostname
3446 issue = self.GetIssue()
3447 patchset = int(patchset or self.GetPatchset())
3448 data = self._GetChangeDetail(['ALL_REVISIONS'])
Edward Lemurd4d1ba42019-09-20 21:46:37 +00003449
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003450 assert host and issue and patchset, 'CL must be uploaded first'
Edward Lemurd4d1ba42019-09-20 21:46:37 +00003451
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003452 has_patchset = any(
3453 int(revision_data['_number']) == patchset
3454 for revision_data in data['revisions'].values())
3455 if not has_patchset:
3456 raise Exception('Patchset %d is not known in Gerrit change %d' %
3457 (patchset, self.GetIssue()))
Edward Lemurd4d1ba42019-09-20 21:46:37 +00003458
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003459 return {
3460 'host': host,
3461 'change': issue,
3462 'project': data['project'],
3463 'patchset': patchset,
3464 }
tandriie113dfd2016-10-11 10:20:12 -07003465
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003466 def GetIssueOwner(self):
3467 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003468
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003469 def GetReviewers(self):
3470 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
3471 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01003472
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003473
Lei Zhang8a0efc12020-08-05 19:58:45 +00003474def _get_bug_line_values(default_project_prefix, bugs):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003475 """Given default_project_prefix and comma separated list of bugs, yields bug
Lei Zhang8a0efc12020-08-05 19:58:45 +00003476 line values.
tandriif9aefb72016-07-01 09:06:51 -07003477
3478 Each bug can be either:
Lei Zhang8a0efc12020-08-05 19:58:45 +00003479 * a number, which is combined with default_project_prefix
tandriif9aefb72016-07-01 09:06:51 -07003480 * string, which is left as is.
3481
3482 This function may produce more than one line, because bugdroid expects one
3483 project per line.
3484
Lei Zhang8a0efc12020-08-05 19:58:45 +00003485 >>> list(_get_bug_line_values('v8:', '123,chromium:789'))
tandriif9aefb72016-07-01 09:06:51 -07003486 ['v8:123', 'chromium:789']
3487 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003488 default_bugs = []
3489 others = []
3490 for bug in bugs.split(','):
3491 bug = bug.strip()
3492 if bug:
3493 try:
3494 default_bugs.append(int(bug))
3495 except ValueError:
3496 others.append(bug)
tandriif9aefb72016-07-01 09:06:51 -07003497
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003498 if default_bugs:
3499 default_bugs = ','.join(map(str, default_bugs))
3500 if default_project_prefix:
3501 if not default_project_prefix.endswith(':'):
3502 default_project_prefix += ':'
3503 yield '%s%s' % (default_project_prefix, default_bugs)
3504 else:
3505 yield default_bugs
3506 for other in sorted(others):
3507 # Don't bother finding common prefixes, CLs with >2 bugs are very very
3508 # rare.
3509 yield other
tandriif9aefb72016-07-01 09:06:51 -07003510
3511
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003512def FindCodereviewSettingsFile(filename='codereview.settings'):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003513 """Finds the given file starting in the cwd and going up.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003514
3515 Only looks up to the top of the repository unless an
3516 'inherit-review-settings-ok' file exists in the root of the repository.
3517 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003518 inherit_ok_file = 'inherit-review-settings-ok'
3519 cwd = os.getcwd()
3520 root = settings.GetRoot()
3521 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3522 root = None
3523 while True:
3524 if os.path.isfile(os.path.join(cwd, filename)):
3525 return open(os.path.join(cwd, filename))
3526 if cwd == root:
3527 break
3528 parent_dir = os.path.dirname(cwd)
3529 if parent_dir == cwd:
3530 # We hit the system root directory.
3531 break
3532 cwd = parent_dir
3533 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003534
3535
3536def LoadCodereviewSettingsFromFile(fileobj):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003537 """Parses a codereview.settings file and updates hooks."""
3538 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003539
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003540 def SetProperty(name, setting, unset_error_ok=False):
3541 fullname = 'rietveld.' + name
3542 if setting in keyvals:
3543 RunGit(['config', fullname, keyvals[setting]])
3544 else:
3545 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003546
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003547 if not keyvals.get('GERRIT_HOST', False):
3548 SetProperty('server', 'CODE_REVIEW_SERVER')
3549 # Only server setting is required. Other settings can be absent.
3550 # In that case, we ignore errors raised during option deletion attempt.
3551 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
3552 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3553 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
3554 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
3555 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3556 SetProperty('cpplint-ignore-regex',
3557 'LINT_IGNORE_REGEX',
3558 unset_error_ok=True)
3559 SetProperty('run-post-upload-hook',
3560 'RUN_POST_UPLOAD_HOOK',
3561 unset_error_ok=True)
3562 SetProperty('format-full-by-default',
3563 'FORMAT_FULL_BY_DEFAULT',
3564 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003565
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003566 if 'GERRIT_HOST' in keyvals:
3567 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003568
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003569 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
3570 RunGit([
3571 'config', 'gerrit.squash-uploads', keyvals['GERRIT_SQUASH_UPLOADS']
3572 ])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003573
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003574 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
3575 RunGit([
3576 'config', 'gerrit.skip-ensure-authenticated',
3577 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']
3578 ])
tandrii@chromium.org28253532016-04-14 13:46:56 +00003579
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003580 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3581 # should be of the form
3582 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3583 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
3584 RunGit([
3585 'config', keyvals['PUSH_URL_CONFIG'], keyvals['ORIGIN_URL_CONFIG']
3586 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003587
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003588
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003589def urlretrieve(source, destination):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003590 """Downloads a network object to a local file, like urllib.urlretrieve.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003591
3592 This is necessary because urllib is broken for SSL connections via a proxy.
3593 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003594 with open(destination, 'wb') as f:
3595 f.write(urllib.request.urlopen(source).read())
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003596
3597
ukai@chromium.org712d6102013-11-27 00:52:58 +00003598def hasSheBang(fname):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003599 """Checks fname is a #! script."""
3600 with open(fname) as f:
3601 return f.read(2).startswith('#!')
ukai@chromium.org712d6102013-11-27 00:52:58 +00003602
3603
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003604def DownloadGerritHook(force):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003605 """Downloads and installs a Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003606
3607 Args:
3608 force: True to update hooks. False to install hooks if not present.
3609 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003610 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
3611 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3612 if not os.access(dst, os.X_OK):
3613 if os.path.exists(dst):
3614 if not force:
3615 return
3616 try:
3617 urlretrieve(src, dst)
3618 if not hasSheBang(dst):
3619 DieWithError('Not a script: %s\n'
3620 'You need to download from\n%s\n'
3621 'into .git/hooks/commit-msg and '
3622 'chmod +x .git/hooks/commit-msg' % (dst, src))
3623 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3624 except Exception:
3625 if os.path.exists(dst):
3626 os.remove(dst)
3627 DieWithError('\nFailed to download hooks.\n'
3628 'You need to download from\n%s\n'
3629 'into .git/hooks/commit-msg and '
3630 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003631
3632
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003633class _GitCookiesChecker(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003634 """Provides facilities for validating and suggesting fixes to .gitcookies."""
3635 def __init__(self):
3636 # Cached list of [host, identity, source], where source is either
3637 # .gitcookies or .netrc.
3638 self._all_hosts = None
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003639
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003640 def ensure_configured_gitcookies(self):
3641 """Runs checks and suggests fixes to make git use .gitcookies from default
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003642 path."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003643 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3644 configured_path = RunGitSilent(
3645 ['config', '--global', 'http.cookiefile']).strip()
3646 configured_path = os.path.expanduser(configured_path)
3647 if configured_path:
3648 self._ensure_default_gitcookies_path(configured_path, default)
3649 else:
3650 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003651
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003652 @staticmethod
3653 def _ensure_default_gitcookies_path(configured_path, default_path):
3654 assert configured_path
3655 if configured_path == default_path:
3656 print('git is already configured to use your .gitcookies from %s' %
3657 configured_path)
3658 return
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003659
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003660 print('WARNING: You have configured custom path to .gitcookies: %s\n'
3661 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3662 (configured_path, default_path))
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003663
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003664 if not os.path.exists(configured_path):
3665 print('However, your configured .gitcookies file is missing.')
3666 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3667 action='reconfigure')
3668 RunGit(['config', '--global', 'http.cookiefile', default_path])
3669 return
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003670
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003671 if os.path.exists(default_path):
3672 print('WARNING: default .gitcookies file already exists %s' %
3673 default_path)
3674 DieWithError(
3675 'Please delete %s manually and re-run git cl creds-check' %
3676 default_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003677
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003678 confirm_or_exit('Move existing .gitcookies to default location?',
3679 action='move')
3680 shutil.move(configured_path, default_path)
3681 RunGit(['config', '--global', 'http.cookiefile', default_path])
3682 print('Moved and reconfigured git to use .gitcookies from %s' %
3683 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003684
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003685 @staticmethod
3686 def _configure_gitcookies_path(default_path):
3687 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3688 if os.path.exists(netrc_path):
3689 print(
3690 'You seem to be using outdated .netrc for git credentials: %s' %
3691 netrc_path)
3692 print(
3693 'This tool will guide you through setting up recommended '
3694 '.gitcookies store for git credentials.\n'
3695 '\n'
3696 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3697 ' git config --global --unset http.cookiefile\n'
3698 ' mv %s %s.backup\n\n' % (default_path, default_path))
3699 confirm_or_exit(action='setup .gitcookies')
3700 RunGit(['config', '--global', 'http.cookiefile', default_path])
3701 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003702
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003703 def get_hosts_with_creds(self, include_netrc=False):
3704 if self._all_hosts is None:
3705 a = gerrit_util.CookiesAuthenticator()
3706 self._all_hosts = [(h, u, s) for h, u, s in itertools.chain((
3707 (h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.items()), (
3708 (h, u, '.gitcookies')
3709 for h, (u, _) in a.gitcookies.items()))
3710 if h.endswith(_GOOGLESOURCE)]
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003711
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003712 if include_netrc:
3713 return self._all_hosts
3714 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003715
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003716 def print_current_creds(self, include_netrc=False):
3717 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3718 if not hosts:
3719 print('No Git/Gerrit credentials found')
3720 return
3721 lengths = [max(map(len, (row[i] for row in hosts))) for i in range(3)]
3722 header = [('Host', 'User', 'Which file'), ['=' * l for l in lengths]]
3723 for row in (header + hosts):
3724 print('\t'.join((('%%+%ds' % l) % s) for l, s in zip(lengths, row)))
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003725
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003726 @staticmethod
3727 def _parse_identity(identity):
3728 """Parses identity "git-<username>.domain" into <username> and domain."""
3729 # Special case: usernames that contain ".", which are generally not
3730 # distinguishable from sub-domains. But we do know typical domains:
3731 if identity.endswith('.chromium.org'):
3732 domain = 'chromium.org'
3733 username = identity[:-len('.chromium.org')]
3734 else:
3735 username, domain = identity.split('.', 1)
3736 if username.startswith('git-'):
3737 username = username[len('git-'):]
3738 return username, domain
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003739
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003740 def has_generic_host(self):
3741 """Returns whether generic .googlesource.com has been configured.
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003742
3743 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3744 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003745 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3746 if host == '.' + _GOOGLESOURCE:
3747 return True
3748 return False
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003749
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003750 def _get_git_gerrit_identity_pairs(self):
3751 """Returns map from canonic host to pair of identities (Git, Gerrit).
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003752
3753 One of identities might be None, meaning not configured.
3754 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003755 host_to_identity_pairs = {}
3756 for host, identity, _ in self.get_hosts_with_creds():
3757 canonical = _canonical_git_googlesource_host(host)
3758 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3759 idx = 0 if canonical == host else 1
3760 pair[idx] = identity
3761 return host_to_identity_pairs
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003762
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003763 def get_partially_configured_hosts(self):
3764 return set(
3765 (host if i1 else _canonical_gerrit_googlesource_host(host))
3766 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().items()
3767 if None in (i1, i2) and host != '.' + _GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003768
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003769 def get_conflicting_hosts(self):
3770 return set(
3771 host
3772 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().items()
3773 if None not in (i1, i2) and i1 != i2)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003774
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003775 def get_duplicated_hosts(self):
3776 counters = collections.Counter(
3777 h for h, _, _ in self.get_hosts_with_creds())
3778 return set(host for host, count in counters.items() if count > 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003779
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003780 @staticmethod
3781 def _format_hosts(hosts, extra_column_func=None):
3782 hosts = sorted(hosts)
3783 assert hosts
3784 if extra_column_func is None:
3785 extras = [''] * len(hosts)
3786 else:
3787 extras = [extra_column_func(host) for host in hosts]
3788 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len,
3789 extras)))
3790 lines = []
3791 for he in zip(hosts, extras):
3792 lines.append(tmpl % he)
3793 return lines
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003794
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003795 def _find_problems(self):
3796 if self.has_generic_host():
3797 yield ('.googlesource.com wildcard record detected', [
3798 'Chrome Infrastructure team recommends to list full host names '
3799 'explicitly.'
3800 ], None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003801
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003802 dups = self.get_duplicated_hosts()
3803 if dups:
3804 yield ('The following hosts were defined twice',
3805 self._format_hosts(dups), None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003806
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003807 partial = self.get_partially_configured_hosts()
3808 if partial:
3809 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3810 'These hosts are missing',
3811 self._format_hosts(
3812 partial, lambda host: 'but %s defined' %
3813 _get_counterpart_host(host)), partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003814
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003815 conflicting = self.get_conflicting_hosts()
3816 if conflicting:
3817 yield (
3818 'The following Git hosts have differing credentials from their '
3819 'Gerrit counterparts',
3820 self._format_hosts(
3821 conflicting, lambda host: '%s vs %s' % tuple(
3822 self._get_git_gerrit_identity_pairs()[host])),
3823 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003824
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003825 def find_and_report_problems(self):
3826 """Returns True if there was at least one problem, else False."""
3827 found = False
3828 bad_hosts = set()
3829 for title, sublines, hosts in self._find_problems():
3830 if not found:
3831 found = True
3832 print('\n\n.gitcookies problem report:\n')
3833 bad_hosts.update(hosts or [])
3834 print(' %s%s' % (title, (':' if sublines else '')))
3835 if sublines:
3836 print()
3837 print(' %s' % '\n '.join(sublines))
3838 print()
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003839
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003840 if bad_hosts:
3841 assert found
3842 print(
3843 ' You can manually remove corresponding lines in your %s file and '
3844 'visit the following URLs with correct account to generate '
3845 'correct credential lines:\n' %
3846 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
3847 print(' %s' % '\n '.join(
3848 sorted(
3849 set(gerrit_util.CookiesAuthenticator().get_new_password_url(
3850 _canonical_git_googlesource_host(host))
3851 for host in bad_hosts))))
3852 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003853
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003854
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003855@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003856def CMDcreds_check(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003857 """Checks credentials and suggests changes."""
3858 _, _ = parser.parse_args(args)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003859
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003860 # Code below checks .gitcookies. Abort if using something else.
3861 authn = gerrit_util.Authenticator.get()
3862 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
3863 message = (
3864 'This command is not designed for bot environment. It checks '
3865 '~/.gitcookies file not generally used on bots.')
3866 # TODO(crbug.com/1059384): Automatically detect when running on
3867 # cloudtop.
3868 if isinstance(authn, gerrit_util.GceAuthenticator):
3869 message += (
3870 '\n'
3871 'If you need to run this on GCE or a cloudtop instance, '
3872 'export SKIP_GCE_AUTH_FOR_GIT=1 in your env.')
3873 DieWithError(message)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003874
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003875 checker = _GitCookiesChecker()
3876 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003877
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003878 print('Your .netrc and .gitcookies have credentials for these hosts:')
3879 checker.print_current_creds(include_netrc=True)
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003880
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003881 if not checker.find_and_report_problems():
3882 print('\nNo problems detected in your .gitcookies file.')
3883 return 0
3884 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003885
3886
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003887@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003888def CMDbaseurl(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003889 """Gets or sets base-url for this branch."""
3890 _, args = parser.parse_args(args)
3891 branchref = scm.GIT.GetBranchRef(settings.GetRoot())
3892 branch = scm.GIT.ShortBranchName(branchref)
3893 if not args:
3894 print('Current base-url:')
3895 return RunGit(['config', 'branch.%s.base-url' % branch],
3896 error_ok=False).strip()
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00003897
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003898 print('Setting base-url to %s' % args[0])
3899 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3900 error_ok=False).strip()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003901
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003902
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003903def color_for_status(status):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003904 """Maps a Changelist status to color, for CMDstatus and other tools."""
3905 BOLD = '\033[1m'
3906 return {
3907 'unsent': BOLD + Fore.YELLOW,
3908 'waiting': BOLD + Fore.RED,
3909 'reply': BOLD + Fore.YELLOW,
3910 'not lgtm': BOLD + Fore.RED,
3911 'lgtm': BOLD + Fore.GREEN,
3912 'commit': BOLD + Fore.MAGENTA,
3913 'closed': BOLD + Fore.CYAN,
3914 'error': BOLD + Fore.WHITE,
3915 }.get(status, Fore.WHITE)
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003916
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003917
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003918def get_cl_statuses(changes, fine_grained, max_processes=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003919 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003920
3921 If fine_grained is true, this will fetch CL statuses from the server.
3922 Otherwise, simply indicate if there's a matching url for the given branches.
3923
3924 If max_processes is specified, it is used as the maximum number of processes
3925 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3926 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003927
3928 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003929 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003930 if not changes:
3931 return
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003932
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003933 if not fine_grained:
3934 # Fast path which doesn't involve querying codereview servers.
3935 # Do not use get_approving_reviewers(), since it requires an HTTP
3936 # request.
3937 for cl in changes:
3938 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
3939 return
3940
3941 # First, sort out authentication issues.
3942 logging.debug('ensuring credentials exist')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003943 for cl in changes:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003944 cl.EnsureAuthenticated(force=False, refresh=True)
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003945
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003946 def fetch(cl):
3947 try:
3948 return (cl, cl.GetStatus())
3949 except:
3950 # See http://crbug.com/629863.
3951 logging.exception('failed to fetch status for cl %s:',
3952 cl.GetIssue())
3953 raise
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003954
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003955 threads_count = len(changes)
3956 if max_processes:
3957 threads_count = max(1, min(threads_count, max_processes))
3958 logging.debug('querying %d CLs using %d threads', len(changes),
3959 threads_count)
3960
3961 pool = multiprocessing.pool.ThreadPool(threads_count)
3962 fetched_cls = set()
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003963 try:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003964 it = pool.imap_unordered(fetch, changes).__iter__()
3965 while True:
3966 try:
3967 cl, status = it.next(timeout=5)
3968 except (multiprocessing.TimeoutError, StopIteration):
3969 break
3970 fetched_cls.add(cl)
3971 yield cl, status
3972 finally:
3973 pool.close()
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003974
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003975 # Add any branches that failed to fetch.
3976 for cl in set(changes) - fetched_cls:
3977 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003978
rmistry@google.com2dd99862015-06-22 12:22:18 +00003979
Jose Lopes3863fc52020-04-07 17:00:25 +00003980def upload_branch_deps(cl, args, force=False):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003981 """Uploads CLs of local branches that are dependents of the current branch.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003982
3983 If the local branch dependency tree looks like:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003984
3985 test1 -> test2.1 -> test3.1
3986 -> test3.2
3987 -> test2.2 -> test3.3
rmistry@google.com2dd99862015-06-22 12:22:18 +00003988
3989 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3990 run on the dependent branches in this order:
3991 test2.1, test3.1, test3.2, test2.2, test3.3
3992
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003993 Note: This function does not rebase your local dependent branches. Use it
3994 when you make a change to the parent branch that will not conflict
3995 with its dependent branches, and you would like their dependencies
3996 updated in Rietveld.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003997 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003998 if git_common.is_dirty_git_tree('upload-branch-deps'):
3999 return 1
rmistry@google.com2dd99862015-06-22 12:22:18 +00004000
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004001 root_branch = cl.GetBranch()
4002 if root_branch is None:
4003 DieWithError('Can\'t find dependent branches from detached HEAD state. '
4004 'Get on a branch!')
4005 if not cl.GetIssue():
4006 DieWithError(
4007 'Current branch does not have an uploaded CL. We cannot set '
4008 'patchset dependencies without an uploaded CL.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004009
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004010 branches = RunGit([
4011 'for-each-ref', '--format=%(refname:short) %(upstream:short)',
4012 'refs/heads'
4013 ])
4014 if not branches:
4015 print('No local branches found.')
4016 return 0
rmistry@google.com2dd99862015-06-22 12:22:18 +00004017
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004018 # Create a dictionary of all local branches to the branches that are
4019 # dependent on it.
4020 tracked_to_dependents = collections.defaultdict(list)
4021 for b in branches.splitlines():
4022 tokens = b.split()
4023 if len(tokens) == 2:
4024 branch_name, tracked = tokens
4025 tracked_to_dependents[tracked].append(branch_name)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004026
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004027 print()
4028 print('The dependent local branches of %s are:' % root_branch)
4029 dependents = []
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004030
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004031 def traverse_dependents_preorder(branch, padding=''):
4032 dependents_to_process = tracked_to_dependents.get(branch, [])
4033 padding += ' '
4034 for dependent in dependents_to_process:
4035 print('%s%s' % (padding, dependent))
4036 dependents.append(dependent)
4037 traverse_dependents_preorder(dependent, padding)
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004038
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004039 traverse_dependents_preorder(root_branch)
4040 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004041
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004042 if not dependents:
4043 print('There are no dependent local branches for %s' % root_branch)
4044 return 0
rmistry@google.com2dd99862015-06-22 12:22:18 +00004045
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004046 # Record all dependents that failed to upload.
4047 failures = {}
4048 # Go through all dependents, checkout the branch and upload.
4049 try:
4050 for dependent_branch in dependents:
4051 print()
4052 print('--------------------------------------')
4053 print('Running "git cl upload" from %s:' % dependent_branch)
4054 RunGit(['checkout', '-q', dependent_branch])
4055 print()
4056 try:
4057 if CMDupload(OptionParser(), args) != 0:
4058 print('Upload failed for %s!' % dependent_branch)
4059 failures[dependent_branch] = 1
4060 except: # pylint: disable=bare-except
4061 failures[dependent_branch] = 1
4062 print()
4063 finally:
4064 # Swap back to the original root branch.
4065 RunGit(['checkout', '-q', root_branch])
4066
4067 print()
4068 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004069 for dependent_branch in dependents:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004070 upload_status = 'failed' if failures.get(
4071 dependent_branch) else 'succeeded'
4072 print(' %s : %s' % (dependent_branch, upload_status))
4073 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004074
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004075 return 0
rmistry@google.com2dd99862015-06-22 12:22:18 +00004076
4077
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00004078def GetArchiveTagForBranch(issue_num, branch_name, existing_tags, pattern):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004079 """Given a proposed tag name, returns a tag name that is guaranteed to be
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00004080 unique. If 'foo' is proposed but already exists, then 'foo-2' is used,
4081 or 'foo-3', and so on."""
4082
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004083 proposed_tag = pattern.format(**{'issue': issue_num, 'branch': branch_name})
4084 for suffix_num in itertools.count(1):
4085 if suffix_num == 1:
4086 to_check = proposed_tag
4087 else:
4088 to_check = '%s-%d' % (proposed_tag, suffix_num)
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00004089
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004090 if to_check not in existing_tags:
4091 return to_check
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00004092
4093
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004094@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07004095def CMDarchive(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004096 """Archives and deletes branches associated with closed changelists."""
4097 parser.add_option(
4098 '-j',
4099 '--maxjobs',
4100 action='store',
4101 type=int,
4102 help='The maximum number of jobs to use when retrieving review status.')
4103 parser.add_option('-f',
4104 '--force',
4105 action='store_true',
4106 help='Bypasses the confirmation prompt.')
4107 parser.add_option('-d',
4108 '--dry-run',
4109 action='store_true',
4110 help='Skip the branch tagging and removal steps.')
4111 parser.add_option('-t',
4112 '--notags',
4113 action='store_true',
4114 help='Do not tag archived branches. '
4115 'Note: local commit history may be lost.')
4116 parser.add_option('-p',
4117 '--pattern',
4118 default='git-cl-archived-{issue}-{branch}',
4119 help='Format string for archive tags. '
4120 'E.g. \'archived-{issue}-{branch}\'.')
kmarshall3bff56b2016-06-06 18:31:47 -07004121
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004122 options, args = parser.parse_args(args)
4123 if args:
4124 parser.error('Unsupported args: %s' % ' '.join(args))
kmarshall3bff56b2016-06-06 18:31:47 -07004125
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004126 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4127 if not branches:
4128 return 0
4129
4130 tags = RunGit(['for-each-ref', '--format=%(refname)', 'refs/tags'
4131 ]).splitlines() or []
4132 tags = [t.split('/')[-1] for t in tags]
4133
4134 print('Finding all branches associated with closed issues...')
4135 changes = [Changelist(branchref=b) for b in branches.splitlines()]
4136 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4137 statuses = get_cl_statuses(changes,
4138 fine_grained=True,
4139 max_processes=options.maxjobs)
4140 proposal = [(cl.GetBranch(),
4141 GetArchiveTagForBranch(cl.GetIssue(), cl.GetBranch(), tags,
4142 options.pattern))
4143 for cl, status in statuses
4144 if status in ('closed', 'rietveld-not-supported')]
4145 proposal.sort()
4146
4147 if not proposal:
4148 print('No branches with closed codereview issues found.')
4149 return 0
4150
4151 current_branch = scm.GIT.GetBranch(settings.GetRoot())
4152
4153 print('\nBranches with closed issues that will be archived:\n')
4154 if options.notags:
4155 for next_item in proposal:
4156 print(' ' + next_item[0])
4157 else:
4158 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4159 for next_item in proposal:
4160 print('%*s %s' % (alignment, next_item[0], next_item[1]))
4161
4162 # Quit now on precondition failure or if instructed by the user, either
4163 # via an interactive prompt or by command line flags.
4164 if options.dry_run:
4165 print('\nNo changes were made (dry run).\n')
4166 return 0
4167
4168 if any(branch == current_branch for branch, _ in proposal):
4169 print('You are currently on a branch \'%s\' which is associated with a '
4170 'closed codereview issue, so archive cannot proceed. Please '
4171 'checkout another branch and run this command again.' %
4172 current_branch)
4173 return 1
4174
4175 if not options.force:
4176 answer = gclient_utils.AskForData(
4177 '\nProceed with deletion (Y/n)? ').lower()
4178 if answer not in ('y', ''):
4179 print('Aborted.')
4180 return 1
4181
4182 for branch, tagname in proposal:
4183 if not options.notags:
4184 RunGit(['tag', tagname, branch])
4185
4186 if RunGitWithCode(['branch', '-D', branch])[0] != 0:
4187 # Clean up the tag if we failed to delete the branch.
4188 RunGit(['tag', '-d', tagname])
4189
4190 print('\nJob\'s done!')
4191
kmarshall3bff56b2016-06-06 18:31:47 -07004192 return 0
4193
kmarshall3bff56b2016-06-06 18:31:47 -07004194
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004195@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004196def CMDstatus(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004197 """Show status of changelists.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004198
4199 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004200 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004201 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004202 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004203 - Red 'not LGTM'ed
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004204 - Magenta in the CQ
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004205 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004206 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004207
4208 Also see 'git cl comments'.
4209 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004210 parser.add_option('--no-branch-color',
4211 action='store_true',
4212 help='Disable colorized branch names')
4213 parser.add_option(
4214 '--field', help='print only specific field (desc|id|patch|status|url)')
4215 parser.add_option('-f',
4216 '--fast',
4217 action='store_true',
4218 help='Do not retrieve review status')
4219 parser.add_option(
4220 '-j',
4221 '--maxjobs',
4222 action='store',
4223 type=int,
4224 help='The maximum number of jobs to use when retrieving review status')
4225 parser.add_option(
4226 '-i',
4227 '--issue',
4228 type=int,
4229 help='Operate on this issue instead of the current branch\'s implicit '
4230 'issue. Requires --field to be set.')
4231 parser.add_option('-d',
4232 '--date-order',
4233 action='store_true',
4234 help='Order branches by committer date.')
4235 options, args = parser.parse_args(args)
4236 if args:
4237 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004238
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004239 if options.issue is not None and not options.field:
4240 parser.error('--field must be given when --issue is set.')
iannucci3c972b92016-08-17 13:24:10 -07004241
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004242 if options.field:
4243 cl = Changelist(issue=options.issue)
4244 if options.field.startswith('desc'):
4245 if cl.GetIssue():
4246 print(cl.FetchDescription())
4247 elif options.field == 'id':
4248 issueid = cl.GetIssue()
4249 if issueid:
4250 print(issueid)
4251 elif options.field == 'patch':
4252 patchset = cl.GetMostRecentPatchset()
4253 if patchset:
4254 print(patchset)
4255 elif options.field == 'status':
4256 print(cl.GetStatus())
4257 elif options.field == 'url':
4258 url = cl.GetIssueURL()
4259 if url:
4260 print(url)
4261 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004262
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004263 branches = RunGit([
4264 'for-each-ref', '--format=%(refname) %(committerdate:unix)',
4265 'refs/heads'
4266 ])
4267 if not branches:
4268 print('No local branch found.')
4269 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004270
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004271 changes = [
4272 Changelist(branchref=b, commit_date=ct)
4273 for b, ct in map(lambda line: line.split(' '), branches.splitlines())
4274 ]
4275 print('Branches associated with reviews:')
4276 output = get_cl_statuses(changes,
4277 fine_grained=not options.fast,
4278 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004279
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004280 current_branch = scm.GIT.GetBranch(settings.GetRoot())
Daniel McArdlea23bf592019-02-12 00:25:12 +00004281
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004282 def FormatBranchName(branch, colorize=False):
4283 """Simulates 'git branch' behavior. Colorizes and prefixes branch name with
Daniel McArdlea23bf592019-02-12 00:25:12 +00004284 an asterisk when it is the current branch."""
4285
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004286 asterisk = ""
4287 color = Fore.RESET
4288 if branch == current_branch:
4289 asterisk = "* "
4290 color = Fore.GREEN
4291 branch_name = scm.GIT.ShortBranchName(branch)
Daniel McArdlea23bf592019-02-12 00:25:12 +00004292
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004293 if colorize:
4294 return asterisk + color + branch_name + Fore.RESET
4295 return asterisk + branch_name
Daniel McArdle452a49f2019-02-14 17:28:31 +00004296
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004297 branch_statuses = {}
Daniel McArdlea23bf592019-02-12 00:25:12 +00004298
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004299 alignment = max(5,
4300 max(len(FormatBranchName(c.GetBranch())) for c in changes))
Sigurd Schneider1bfda8e2021-06-30 14:46:25 +00004301
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004302 if options.date_order or settings.IsStatusCommitOrderByDate():
4303 sorted_changes = sorted(changes,
4304 key=lambda c: c.GetCommitDate(),
4305 reverse=True)
4306 else:
4307 sorted_changes = sorted(changes, key=lambda c: c.GetBranch())
4308 for cl in sorted_changes:
4309 branch = cl.GetBranch()
4310 while branch not in branch_statuses:
4311 c, status = next(output)
4312 branch_statuses[c.GetBranch()] = status
4313 status = branch_statuses.pop(branch)
4314 url = cl.GetIssueURL(short=True)
4315 if url and (not status or status == 'error'):
4316 # The issue probably doesn't exist anymore.
4317 url += ' (broken)'
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004318
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004319 color = color_for_status(status)
4320 # Turn off bold as well as colors.
4321 END = '\033[0m'
4322 reset = Fore.RESET + END
4323 if not setup_color.IS_TTY:
4324 color = ''
4325 reset = ''
4326 status_str = '(%s)' % status if status else ''
Daniel McArdle452a49f2019-02-14 17:28:31 +00004327
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004328 branch_display = FormatBranchName(branch)
4329 padding = ' ' * (alignment - len(branch_display))
4330 if not options.no_branch_color:
4331 branch_display = FormatBranchName(branch, colorize=True)
Daniel McArdle452a49f2019-02-14 17:28:31 +00004332
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004333 print(' %s : %s%s %s%s' %
4334 (padding + branch_display, color, url, status_str, reset))
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004335
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004336 print()
4337 print('Current branch: %s' % current_branch)
4338 for cl in changes:
4339 if cl.GetBranch() == current_branch:
4340 break
4341 if not cl.GetIssue():
4342 print('No issue assigned.')
4343 return 0
4344 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4345 if not options.fast:
4346 print('Issue description:')
4347 print(cl.FetchDescription(pretty=True))
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004348 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004349
4350
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004351def colorize_CMDstatus_doc():
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004352 """To be called once in main() to add colors to git cl status help."""
4353 colors = [i for i in dir(Fore) if i[0].isupper()]
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004354
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004355 def colorize_line(line):
4356 for color in colors:
4357 if color in line.upper():
4358 # Extract whitespace first and the leading '-'.
4359 indent = len(line) - len(line.lstrip(' ')) + 1
4360 return line[:indent] + getattr(
4361 Fore, color) + line[indent:] + Fore.RESET
4362 return line
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004363
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004364 lines = CMDstatus.__doc__.splitlines()
4365 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004366
4367
phajdan.jre328cf92016-08-22 04:12:17 -07004368def write_json(path, contents):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004369 if path == '-':
4370 json.dump(contents, sys.stdout)
4371 else:
4372 with open(path, 'w') as f:
4373 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004374
4375
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004376@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004377@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004378def CMDissue(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004379 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004380
4381 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004382 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004383 parser.add_option('-r',
4384 '--reverse',
4385 action='store_true',
4386 help='Lookup the branch(es) for the specified issues. If '
4387 'no issues are specified, all branches with mapped '
4388 'issues will be listed.')
4389 parser.add_option('--json',
4390 help='Path to JSON output file, or "-" for stdout.')
4391 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004392
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004393 if options.reverse:
4394 branches = RunGit(['for-each-ref', 'refs/heads',
4395 '--format=%(refname)']).splitlines()
4396 # Reverse issue lookup.
4397 issue_branch_map = {}
Arthur Milchior801a9752023-04-07 10:33:54 +00004398
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004399 git_config = {}
4400 for config in RunGit(['config', '--get-regexp',
4401 r'branch\..*issue']).splitlines():
4402 name, _space, val = config.partition(' ')
4403 git_config[name] = val
Arthur Milchior801a9752023-04-07 10:33:54 +00004404
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004405 for branch in branches:
4406 issue = git_config.get(
4407 'branch.%s.%s' %
4408 (scm.GIT.ShortBranchName(branch), ISSUE_CONFIG_KEY))
4409 if issue:
4410 issue_branch_map.setdefault(int(issue), []).append(branch)
4411 if not args:
4412 args = sorted(issue_branch_map.keys())
4413 result = {}
4414 for issue in args:
4415 try:
4416 issue_num = int(issue)
4417 except ValueError:
4418 print('ERROR cannot parse issue number: %s' % issue,
4419 file=sys.stderr)
4420 continue
4421 result[issue_num] = issue_branch_map.get(issue_num)
4422 print('Branch for issue number %s: %s' % (issue, ', '.join(
4423 issue_branch_map.get(issue_num) or ('None', ))))
4424 if options.json:
4425 write_json(options.json, result)
4426 return 0
4427
4428 if len(args) > 0:
4429 issue = ParseIssueNumberArgument(args[0])
4430 if not issue.valid:
4431 DieWithError(
4432 'Pass a url or number to set the issue, 0 to unset it, '
4433 'or no argument to list it.\n'
4434 'Maybe you want to run git cl status?')
4435 cl = Changelist()
4436 cl.SetIssue(issue.issue)
4437 else:
4438 cl = Changelist()
4439 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
Arthur Milchior801a9752023-04-07 10:33:54 +00004440 if options.json:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004441 write_json(
4442 options.json, {
4443 'gerrit_host': cl.GetGerritHost(),
4444 'gerrit_project': cl.GetGerritProject(),
4445 'issue_url': cl.GetIssueURL(),
4446 'issue': cl.GetIssue(),
4447 })
Arthur Milchior801a9752023-04-07 10:33:54 +00004448 return 0
Aaron Gable78753da2017-06-15 10:35:49 -07004449
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004450
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004451@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004452def CMDcomments(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004453 """Shows or posts review comments for any changelist."""
4454 parser.add_option('-a',
4455 '--add-comment',
4456 dest='comment',
4457 help='comment to add to an issue')
4458 parser.add_option('-p',
4459 '--publish',
4460 action='store_true',
4461 help='marks CL as ready and sends comment to reviewers')
4462 parser.add_option('-i',
4463 '--issue',
4464 dest='issue',
4465 help='review issue id (defaults to current issue).')
4466 parser.add_option('-m',
4467 '--machine-readable',
4468 dest='readable',
4469 action='store_false',
4470 default=True,
4471 help='output comments in a format compatible with '
4472 'editor parsing')
4473 parser.add_option('-j',
4474 '--json-file',
4475 help='File to write JSON summary to, or "-" for stdout')
4476 options, args = parser.parse_args(args)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004477
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004478 issue = None
4479 if options.issue:
4480 try:
4481 issue = int(options.issue)
4482 except ValueError:
4483 DieWithError('A review issue ID is expected to be a number.')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004484
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004485 cl = Changelist(issue=issue)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004486
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004487 if options.comment:
4488 cl.AddComment(options.comment, options.publish)
4489 return 0
4490
4491 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4492 key=lambda c: c.date)
4493 for comment in summary:
4494 if comment.disapproval:
4495 color = Fore.RED
4496 elif comment.approval:
4497 color = Fore.GREEN
4498 elif comment.sender == cl.GetIssueOwner():
4499 color = Fore.MAGENTA
4500 elif comment.autogenerated:
4501 color = Fore.CYAN
4502 else:
4503 color = Fore.BLUE
4504 print('\n%s%s %s%s\n%s' %
4505 (color, comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4506 comment.sender, Fore.RESET, '\n'.join(
4507 ' ' + l for l in comment.message.strip().splitlines())))
4508
4509 if options.json_file:
4510
4511 def pre_serialize(c):
4512 dct = c._asdict().copy()
4513 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4514 return dct
4515
4516 write_json(options.json_file, [pre_serialize(x) for x in summary])
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004517 return 0
4518
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004519
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004520@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004521@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004522def CMDdescription(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004523 """Brings up the editor for the current CL's description."""
4524 parser.add_option(
4525 '-d',
4526 '--display',
4527 action='store_true',
4528 help='Display the description instead of opening an editor')
4529 parser.add_option(
4530 '-n',
4531 '--new-description',
4532 help='New description to set for this issue (- for stdin, '
4533 '+ to load from local commit HEAD)')
4534 parser.add_option('-f',
4535 '--force',
4536 action='store_true',
4537 help='Delete any unpublished Gerrit edits for this issue '
4538 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004539
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004540 options, args = parser.parse_args(args)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004541
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004542 target_issue_arg = None
4543 if len(args) > 0:
4544 target_issue_arg = ParseIssueNumberArgument(args[0])
4545 if not target_issue_arg.valid:
4546 parser.error('Invalid issue ID or URL.')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004547
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004548 kwargs = {}
4549 if target_issue_arg:
4550 kwargs['issue'] = target_issue_arg.issue
4551 kwargs['codereview_host'] = target_issue_arg.hostname
martiniss6eda05f2016-06-30 10:18:35 -07004552
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004553 cl = Changelist(**kwargs)
4554 if not cl.GetIssue():
4555 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004556
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004557 if args and not args[0].isdigit():
4558 logging.info('canonical issue/change URL: %s\n', cl.GetIssueURL())
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004559
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004560 description = ChangeDescription(cl.FetchDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004561
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004562 if options.display:
4563 print(description.description)
4564 return 0
4565
4566 if options.new_description:
4567 text = options.new_description
4568 if text == '-':
4569 text = '\n'.join(l.rstrip() for l in sys.stdin)
4570 elif text == '+':
4571 base_branch = cl.GetCommonAncestorWithUpstream()
4572 text = _create_description_from_log([base_branch])
4573
4574 description.set_description(text)
4575 else:
4576 description.prompt()
4577 if cl.FetchDescription().strip() != description.description:
4578 cl.UpdateDescription(description.description, force=options.force)
smut@google.com34fb6b12015-07-13 20:03:26 +00004579 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004580
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004581
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004582@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004583def CMDlint(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004584 """Runs cpplint on the current changelist."""
4585 parser.add_option(
4586 '--filter',
4587 action='append',
4588 metavar='-x,+y',
4589 help='Comma-separated list of cpplint\'s category-filters')
4590 options, args = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004591
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004592 # Access to a protected member _XX of a client class
4593 # pylint: disable=protected-access
4594 try:
4595 import cpplint
4596 import cpplint_chromium
4597 except ImportError:
4598 print(
4599 'Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.'
4600 )
4601 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004602
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004603 # Change the current working directory before calling lint so that it
4604 # shows the correct base.
4605 previous_cwd = os.getcwd()
4606 os.chdir(settings.GetRoot())
4607 try:
4608 cl = Changelist()
4609 files = cl.GetAffectedFiles(cl.GetCommonAncestorWithUpstream())
4610 if not files:
4611 print('Cannot lint an empty CL')
4612 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004613
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004614 # Process cpplint arguments, if any.
4615 filters = presubmit_canned_checks.GetCppLintFilters(options.filter)
4616 command = ['--filter=' + ','.join(filters)]
4617 command.extend(args)
4618 command.extend(files)
4619 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004620
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004621 include_regex = re.compile(settings.GetLintRegex())
4622 ignore_regex = re.compile(settings.GetLintIgnoreRegex())
4623 extra_check_functions = [
4624 cpplint_chromium.CheckPointerDeclarationWhitespace
4625 ]
4626 for filename in filenames:
4627 if not include_regex.match(filename):
4628 print('Skipping file %s' % filename)
4629 continue
Lei Zhang379d1ad2020-07-15 19:40:06 +00004630
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004631 if ignore_regex.match(filename):
4632 print('Ignoring file %s' % filename)
4633 continue
Lei Zhang379d1ad2020-07-15 19:40:06 +00004634
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004635 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4636 extra_check_functions)
4637 finally:
4638 os.chdir(previous_cwd)
4639 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
4640 if cpplint._cpplint_state.error_count != 0:
4641 return 1
4642 return 0
thestig@chromium.org44202a22014-03-11 19:22:18 +00004643
4644
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004645@metrics.collector.collect_metrics('git cl presubmit')
mlcuic601e362023-08-14 23:39:46 +00004646@subcommand.usage('[base branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004647def CMDpresubmit(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004648 """Runs presubmit tests on the current changelist."""
4649 parser.add_option('-u',
4650 '--upload',
4651 action='store_true',
4652 help='Run upload hook instead of the push hook')
4653 parser.add_option('-f',
4654 '--force',
4655 action='store_true',
4656 help='Run checks even if tree is dirty')
4657 parser.add_option(
4658 '--all',
4659 action='store_true',
4660 help='Run checks against all files, not just modified ones')
4661 parser.add_option('--files',
4662 nargs=1,
4663 help='Semicolon-separated list of files to be marked as '
4664 'modified when executing presubmit or post-upload hooks. '
4665 'fnmatch wildcards can also be used.')
4666 parser.add_option(
4667 '--parallel',
4668 action='store_true',
4669 help='Run all tests specified by input_api.RunTests in all '
4670 'PRESUBMIT files in parallel.')
4671 parser.add_option('--resultdb',
4672 action='store_true',
4673 help='Run presubmit checks in the ResultSink environment '
4674 'and send results to the ResultDB database.')
4675 parser.add_option('--realm', help='LUCI realm if reporting to ResultDB')
4676 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004677
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004678 if not options.force and git_common.is_dirty_git_tree('presubmit'):
4679 print('use --force to check even if tree is dirty.')
4680 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004681
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004682 cl = Changelist()
4683 if args:
4684 base_branch = args[0]
Bruce Dawsoneb8426e2022-08-05 23:58:15 +00004685 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004686 # Default to diffing against the common ancestor of the upstream branch.
4687 base_branch = cl.GetCommonAncestorWithUpstream()
Aaron Gable8076c282017-11-29 14:39:41 -08004688
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004689 start = time.time()
4690 try:
4691 if not 'PRESUBMIT_SKIP_NETWORK' in os.environ and cl.GetIssue():
4692 description = cl.FetchDescription()
4693 else:
4694 description = _create_description_from_log([base_branch])
4695 except Exception as e:
4696 print('Failed to fetch CL description - %s' % str(e))
4697 description = _create_description_from_log([base_branch])
4698 elapsed = time.time() - start
4699 if elapsed > 5:
4700 print('%.1f s to get CL description.' % elapsed)
Bruce Dawson13acea32022-05-03 22:13:08 +00004701
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004702 if not base_branch:
4703 if not options.force:
4704 print('use --force to check even when not on a branch.')
4705 return 1
4706 base_branch = 'HEAD'
4707
4708 cl.RunHook(committing=not options.upload,
4709 may_prompt=False,
4710 verbose=options.verbose,
4711 parallel=options.parallel,
4712 upstream=base_branch,
4713 description=description,
4714 all_files=options.all,
4715 files=options.files,
4716 resultdb=options.resultdb,
4717 realm=options.realm)
4718 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004719
4720
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004721def GenerateGerritChangeId(message):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004722 """Returns the Change ID footer value (Ixxxxxx...xxx).
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004723
4724 Works the same way as
4725 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4726 but can be called on demand on all platforms.
4727
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004728 The basic idea is to generate git hash of a state of the tree, original
4729 commit message, author/committer info and timestamps.
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004730 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004731 lines = []
4732 tree_hash = RunGitSilent(['write-tree'])
4733 lines.append('tree %s' % tree_hash.strip())
4734 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'],
4735 suppress_stderr=False)
4736 if code == 0:
4737 lines.append('parent %s' % parent.strip())
4738 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4739 lines.append('author %s' % author.strip())
4740 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4741 lines.append('committer %s' % committer.strip())
4742 lines.append('')
4743 # Note: Gerrit's commit-hook actually cleans message of some lines and
4744 # whitespace. This code is not doing this, but it clearly won't decrease
4745 # entropy.
4746 lines.append(message)
4747 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4748 stdin=('\n'.join(lines)).encode())
4749 return 'I%s' % change_hash.strip()
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004750
4751
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004752def GetTargetRef(remote, remote_branch, target_branch):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004753 """Computes the remote branch ref to use for the CL.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004754
4755 Args:
4756 remote (str): The git remote for the CL.
4757 remote_branch (str): The git remote branch for the CL.
4758 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004759 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004760 if not (remote and remote_branch):
4761 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004762
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004763 if target_branch:
4764 # Canonicalize branch references to the equivalent local full symbolic
4765 # refs, which are then translated into the remote full symbolic refs
4766 # below.
4767 if '/' not in target_branch:
4768 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4769 else:
4770 prefix_replacements = (
4771 ('^((refs/)?remotes/)?branch-heads/',
4772 'refs/remotes/branch-heads/'),
4773 ('^((refs/)?remotes/)?%s/' % remote,
4774 'refs/remotes/%s/' % remote),
4775 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4776 )
4777 match = None
4778 for regex, replacement in prefix_replacements:
4779 match = re.search(regex, target_branch)
4780 if match:
4781 remote_branch = target_branch.replace(
4782 match.group(0), replacement)
4783 break
4784 if not match:
4785 # This is a branch path but not one we recognize; use as-is.
4786 remote_branch = target_branch
4787 # pylint: disable=consider-using-get
4788 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4789 # pylint: enable=consider-using-get
4790 # Handle the refs that need to land in different refs.
4791 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004792
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004793 # Create the true path to the remote branch.
4794 # Does the following translation:
4795 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4796 # * refs/remotes/origin/main -> refs/heads/main
4797 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4798 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4799 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4800 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4801 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4802 'refs/heads/')
4803 elif remote_branch.startswith('refs/remotes/branch-heads'):
4804 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004805
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004806 return remote_branch
wittman@chromium.org455dc922015-01-26 20:15:50 +00004807
4808
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004809def cleanup_list(l):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004810 """Fixes a list so that comma separated items are put as individual items.
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004811
4812 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4813 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4814 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004815 items = sum((i.split(',') for i in l), [])
4816 stripped_items = (i.strip() for i in items)
4817 return sorted(filter(None, stripped_items))
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004818
4819
Aaron Gable4db38df2017-11-03 14:59:07 -07004820@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004821@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004822def CMDupload(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004823 """Uploads the current changelist to codereview.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004824
4825 Can skip dependency patchset uploads for a branch by running:
4826 git config branch.branch_name.skip-deps-uploads True
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004827 To unset, run:
rmistry@google.com78948ed2015-07-08 23:09:57 +00004828 git config --unset branch.branch_name.skip-deps-uploads
4829 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004830
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004831 If the name of the checked out branch starts with "bug-" or "fix-" followed
4832 by a bug number, this bug number is automatically populated in the CL
Dominic Battre7d1c4842017-10-27 09:17:28 +02004833 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004834
4835 If subject contains text in square brackets or has "<text>: " prefix, such
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004836 text(s) is treated as Gerrit hashtags. For example, CLs with subjects:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004837 [git-cl] add support for hashtags
4838 Foo bar: implement foo
4839 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004840 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004841 parser.add_option('--bypass-hooks',
4842 action='store_true',
4843 dest='bypass_hooks',
4844 help='bypass upload presubmit hook')
4845 parser.add_option('--bypass-watchlists',
4846 action='store_true',
4847 dest='bypass_watchlists',
4848 help='bypass watchlists auto CC-ing reviewers')
4849 parser.add_option('-f',
4850 '--force',
4851 action='store_true',
4852 dest='force',
4853 help="force yes to questions (don't prompt)")
4854 parser.add_option('--message',
4855 '-m',
4856 dest='message',
4857 help='message for patchset')
4858 parser.add_option('-b',
4859 '--bug',
4860 help='pre-populate the bug number(s) for this issue. '
4861 'If several, separate with commas')
4862 parser.add_option('--message-file',
4863 dest='message_file',
4864 help='file which contains message for patchset')
4865 parser.add_option('--title', '-t', dest='title', help='title for patchset')
4866 parser.add_option('-T',
4867 '--skip-title',
4868 action='store_true',
4869 dest='skip_title',
4870 help='Use the most recent commit message as the title of '
4871 'the patchset')
4872 parser.add_option('-r',
4873 '--reviewers',
4874 action='append',
4875 default=[],
4876 help='reviewer email addresses')
4877 parser.add_option('--cc',
4878 action='append',
4879 default=[],
4880 help='cc email addresses')
4881 parser.add_option('--hashtag',
4882 dest='hashtags',
4883 action='append',
4884 default=[],
4885 help=('Gerrit hashtag for new CL; '
4886 'can be applied multiple times'))
4887 parser.add_option('-s',
4888 '--send-mail',
4889 '--send-email',
4890 dest='send_mail',
4891 action='store_true',
4892 help='send email to reviewer(s) and cc(s) immediately')
4893 parser.add_option('--target_branch',
4894 '--target-branch',
4895 metavar='TARGET',
4896 help='Apply CL to remote ref TARGET. ' +
4897 'Default: remote branch head, or main')
4898 parser.add_option('--squash',
4899 action='store_true',
4900 help='Squash multiple commits into one')
4901 parser.add_option('--no-squash',
4902 action='store_false',
4903 dest='squash',
4904 help='Don\'t squash multiple commits into one')
4905 parser.add_option('--topic',
4906 default=None,
4907 help='Topic to specify when uploading')
4908 parser.add_option('--r-owners',
4909 dest='add_owners_to',
4910 action='store_const',
4911 const='R',
4912 help='add a set of OWNERS to R')
4913 parser.add_option('-c',
4914 '--use-commit-queue',
4915 action='store_true',
4916 default=False,
4917 help='tell the CQ to commit this patchset; '
4918 'implies --send-mail')
4919 parser.add_option('-d',
4920 '--cq-dry-run',
4921 action='store_true',
4922 default=False,
4923 help='Send the patchset to do a CQ dry run right after '
4924 'upload.')
4925 parser.add_option('--set-bot-commit',
4926 action='store_true',
4927 help=optparse.SUPPRESS_HELP)
4928 parser.add_option('--preserve-tryjobs',
4929 action='store_true',
4930 help='instruct the CQ to let tryjobs running even after '
4931 'new patchsets are uploaded instead of canceling '
4932 'prior patchset\' tryjobs')
4933 parser.add_option(
4934 '--dependencies',
4935 action='store_true',
4936 help='Uploads CLs of all the local branches that depend on '
4937 'the current branch')
4938 parser.add_option(
4939 '-a',
4940 '--enable-auto-submit',
4941 action='store_true',
4942 help='Sends your change to the CQ after an approval. Only '
4943 'works on repos that have the Auto-Submit label '
4944 'enabled')
4945 parser.add_option(
4946 '--parallel',
4947 action='store_true',
4948 help='Run all tests specified by input_api.RunTests in all '
4949 'PRESUBMIT files in parallel.')
4950 parser.add_option('--no-autocc',
4951 action='store_true',
4952 help='Disables automatic addition of CC emails')
4953 parser.add_option('--private',
4954 action='store_true',
4955 help='Set the review private. This implies --no-autocc.')
4956 parser.add_option('-R',
4957 '--retry-failed',
4958 action='store_true',
4959 help='Retry failed tryjobs from old patchset immediately '
4960 'after uploading new patchset. Cannot be used with '
4961 '--use-commit-queue or --cq-dry-run.')
4962 parser.add_option('--fixed',
4963 '-x',
4964 help='List of bugs that will be commented on and marked '
4965 'fixed (pre-populates "Fixed:" tag). Same format as '
4966 '-b option / "Bug:" tag. If fixing several issues, '
4967 'separate with commas.')
4968 parser.add_option('--edit-description',
4969 action='store_true',
4970 default=False,
4971 help='Modify description before upload. Cannot be used '
4972 'with --force. It is a noop when --no-squash is set '
4973 'or a new commit is created.')
4974 parser.add_option('--git-completion-helper',
4975 action="store_true",
4976 help=optparse.SUPPRESS_HELP)
4977 parser.add_option('-o',
4978 '--push-options',
4979 action='append',
4980 default=[],
4981 help='Transmit the given string to the server when '
4982 'performing git push (pass-through). See git-push '
4983 'documentation for more details.')
4984 parser.add_option('--no-add-changeid',
4985 action='store_true',
4986 dest='no_add_changeid',
4987 help='Do not add change-ids to messages.')
4988 parser.add_option('--cherry-pick-stacked',
4989 '--cp',
4990 dest='cherry_pick_stacked',
4991 action='store_true',
4992 help='If parent branch has un-uploaded updates, '
4993 'automatically skip parent branches and just upload '
4994 'the current branch cherry-pick on its parent\'s last '
4995 'uploaded commit. Allows users to skip the potential '
4996 'interactive confirmation step.')
4997 # TODO(b/265929888): Add --wip option of --cl-status option.
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004998
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004999 orig_args = args
5000 (options, args) = parser.parse_args(args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005001
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005002 if options.git_completion_helper:
5003 print(' '.join(opt.get_opt_string() for opt in parser.option_list
5004 if opt.help != optparse.SUPPRESS_HELP))
5005 return
Ng Zhi Ancdaf0be2020-05-27 20:57:28 +00005006
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005007 # TODO(crbug.com/1475405): Warn users if the project uses submodules and
5008 # they have fsmonitor enabled.
5009 if os.path.isfile('.gitmodules'):
5010 git_common.warn_submodule()
Aravind Vasudevanb8164182023-08-25 21:49:12 +00005011
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005012 if git_common.is_dirty_git_tree('upload'):
5013 return 1
ukai@chromium.orge8077812012-02-03 03:41:46 +00005014
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005015 options.reviewers = cleanup_list(options.reviewers)
5016 options.cc = cleanup_list(options.cc)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005017
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005018 if options.edit_description and options.force:
5019 parser.error('Only one of --force and --edit-description allowed')
Josipe827b0f2020-01-30 00:07:20 +00005020
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005021 if options.message_file:
5022 if options.message:
5023 parser.error('Only one of --message and --message-file allowed.')
5024 options.message = gclient_utils.FileRead(options.message_file)
tandriib80458a2016-06-23 12:20:07 -07005025
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005026 if ([options.cq_dry_run, options.use_commit_queue, options.retry_failed
5027 ].count(True) > 1):
5028 parser.error('Only one of --use-commit-queue, --cq-dry-run or '
5029 '--retry-failed is allowed.')
tandrii4d0545a2016-07-06 03:56:49 -07005030
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005031 if options.skip_title and options.title:
5032 parser.error('Only one of --title and --skip-title allowed.')
Mario Bianuccicebfb4e2020-07-22 23:08:16 +00005033
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005034 if options.use_commit_queue:
5035 options.send_mail = True
Aaron Gableedbc4132017-09-11 13:22:28 -07005036
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005037 if options.squash is None:
5038 # Load default for user, repo, squash=true, in this order.
5039 options.squash = settings.GetSquashGerritUploads()
Edward Lesmes0dd54822020-03-26 18:24:25 +00005040
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005041 cl = Changelist(branchref=options.target_branch)
Joanna Wang5051ffe2023-03-01 22:24:07 +00005042
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005043 # Warm change details cache now to avoid RPCs later, reducing latency for
5044 # developers.
5045 if cl.GetIssue():
5046 cl._GetChangeDetail([
5047 'DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'
5048 ])
Joanna Wang5051ffe2023-03-01 22:24:07 +00005049
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005050 if options.retry_failed and not cl.GetIssue():
5051 print('No previous patchsets, so --retry-failed has no effect.')
5052 options.retry_failed = False
Joanna Wang5051ffe2023-03-01 22:24:07 +00005053
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005054 disable_dogfood_stacked_changes = os.environ.get(
5055 DOGFOOD_STACKED_CHANGES_VAR) == '0'
5056 dogfood_stacked_changes = os.environ.get(DOGFOOD_STACKED_CHANGES_VAR) == '1'
Joanna Wang5051ffe2023-03-01 22:24:07 +00005057
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005058 # Only print message for folks who don't have DOGFOOD_STACKED_CHANGES set
5059 # to an expected value.
5060 if (options.squash and not dogfood_stacked_changes
5061 and not disable_dogfood_stacked_changes):
5062 print(
5063 'This repo has been enrolled in the stacked changes dogfood.\n'
5064 '`git cl upload` now uploads the current branch and all upstream '
5065 'branches that have un-uploaded updates.\n'
5066 'Patches can now be reapplied with --force:\n'
5067 '`git cl patch --reapply --force`.\n'
5068 'Googlers may visit go/stacked-changes-dogfood for more information.\n'
5069 '\n'
5070 'Depot Tools no longer sets new uploads to "WIP". Please update the\n'
5071 '"Set new changes to "work in progress" by default" checkbox at\n'
5072 'https://<host>-review.googlesource.com/settings/\n'
5073 '\n'
5074 'To opt-out use `export DOGFOOD_STACKED_CHANGES=0`.\n'
5075 'To hide this message use `export DOGFOOD_STACKED_CHANGES=1`.\n'
5076 'File bugs at https://bit.ly/3Y6opoI\n')
Joanna Wang4786a412023-05-16 18:23:08 +00005077
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005078 if options.squash and not disable_dogfood_stacked_changes:
5079 if options.dependencies:
5080 parser.error(
5081 '--dependencies is not available for this dogfood workflow.')
Joanna Wang5051ffe2023-03-01 22:24:07 +00005082
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005083 if options.cherry_pick_stacked:
5084 try:
5085 orig_args.remove('--cherry-pick-stacked')
5086 except ValueError:
5087 orig_args.remove('--cp')
5088 UploadAllSquashed(options, orig_args)
5089 return 0
Joanna Wang18de1f62023-01-21 01:24:24 +00005090
Joanna Wangd75fc882023-03-01 21:53:34 +00005091 if options.cherry_pick_stacked:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005092 parser.error(
5093 '--cherry-pick-stacked is not available for this workflow.')
Joanna Wang18de1f62023-01-21 01:24:24 +00005094
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005095 # cl.GetMostRecentPatchset uses cached information, and can return the last
5096 # patchset before upload. Calling it here makes it clear that it's the
5097 # last patchset before upload. Note that GetMostRecentPatchset will fail
5098 # if no CL has been uploaded yet.
5099 if options.retry_failed:
5100 patchset = cl.GetMostRecentPatchset()
Joanna Wangd75fc882023-03-01 21:53:34 +00005101
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005102 ret = cl.CMDUpload(options, args, orig_args)
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00005103
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005104 if options.retry_failed:
5105 if ret != 0:
5106 print('Upload failed, so --retry-failed has no effect.')
5107 return ret
5108 builds, _ = _fetch_latest_builds(cl,
5109 DEFAULT_BUILDBUCKET_HOST,
5110 latest_patchset=patchset)
5111 jobs = _filter_failed_for_retry(builds)
5112 if len(jobs) == 0:
5113 print('No failed tryjobs, so --retry-failed has no effect.')
5114 return ret
5115 _trigger_tryjobs(cl, jobs, options, patchset + 1)
Quinten Yearsleya19d3532019-09-30 21:54:39 +00005116
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005117 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00005118
5119
Daniel Cheng66d0f152023-08-29 23:21:58 +00005120def UploadAllSquashed(options: optparse.Values,
5121 orig_args: Sequence[str]) -> int:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005122 """Uploads the current and upstream branches (if necessary)."""
5123 cls, cherry_pick_current = _UploadAllPrecheck(options, orig_args)
Joanna Wang18de1f62023-01-21 01:24:24 +00005124
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005125 # Create commits.
5126 uploads_by_cl: List[Tuple[Changelist, _NewUpload]] = []
5127 if cherry_pick_current:
5128 parent = cls[1]._GitGetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY)
5129 new_upload = cls[0].PrepareCherryPickSquashedCommit(options, parent)
5130 uploads_by_cl.append((cls[0], new_upload))
5131 else:
5132 ordered_cls = list(reversed(cls))
Joanna Wangc710e2d2023-01-25 14:53:22 +00005133
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005134 cl = ordered_cls[0]
5135 # We can only support external changes when we're only uploading one
5136 # branch.
5137 parent = cl._UpdateWithExternalChanges() if len(
5138 ordered_cls) == 1 else None
5139 orig_parent = None
5140 if parent is None:
5141 origin = '.'
5142 branch = cl.GetBranch()
Joanna Wang74c53b62023-03-01 22:00:22 +00005143
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005144 while origin == '.':
5145 # Search for cl's closest ancestor with a gerrit hash.
5146 origin, upstream_branch_ref = Changelist.FetchUpstreamTuple(
5147 branch)
5148 if origin == '.':
5149 upstream_branch = scm.GIT.ShortBranchName(
5150 upstream_branch_ref)
Joanna Wang7603f042023-03-01 22:17:36 +00005151
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005152 # Support the `git merge` and `git pull` workflow.
5153 if upstream_branch in ['master', 'main']:
5154 parent = cl.GetCommonAncestorWithUpstream()
5155 else:
5156 orig_parent = scm.GIT.GetBranchConfig(
5157 settings.GetRoot(), upstream_branch,
5158 LAST_UPLOAD_HASH_CONFIG_KEY)
5159 parent = scm.GIT.GetBranchConfig(
5160 settings.GetRoot(), upstream_branch,
5161 GERRIT_SQUASH_HASH_CONFIG_KEY)
5162 if parent:
5163 break
5164 branch = upstream_branch
5165 else:
5166 # Either the root of the tree is the cl's direct parent and the
5167 # while loop above only found empty branches between cl and the
5168 # root of the tree.
5169 parent = cl.GetCommonAncestorWithUpstream()
Joanna Wang6215dd02023-02-07 15:58:03 +00005170
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005171 if orig_parent is None:
5172 orig_parent = parent
5173 for i, cl in enumerate(ordered_cls):
5174 # If we're in the middle of the stack, set end_commit to
5175 # downstream's direct ancestor.
5176 if i + 1 < len(ordered_cls):
5177 child_base_commit = ordered_cls[
5178 i + 1].GetCommonAncestorWithUpstream()
5179 else:
5180 child_base_commit = None
5181 new_upload = cl.PrepareSquashedCommit(options,
5182 parent,
5183 orig_parent,
5184 end_commit=child_base_commit)
5185 uploads_by_cl.append((cl, new_upload))
5186 parent = new_upload.commit_to_push
5187 orig_parent = child_base_commit
Joanna Wangc710e2d2023-01-25 14:53:22 +00005188
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005189 # Create refspec options
5190 cl, new_upload = uploads_by_cl[-1]
5191 refspec_opts = cl._GetRefSpecOptions(
5192 options,
5193 new_upload.change_desc,
5194 multi_change_upload=len(uploads_by_cl) > 1,
5195 dogfood_path=True)
5196 refspec_suffix = ''
5197 if refspec_opts:
5198 refspec_suffix = '%' + ','.join(refspec_opts)
5199 assert ' ' not in refspec_suffix, (
5200 'spaces not allowed in refspec: "%s"' % refspec_suffix)
Joanna Wangc710e2d2023-01-25 14:53:22 +00005201
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005202 remote, remote_branch = cl.GetRemoteBranch()
5203 branch = GetTargetRef(remote, remote_branch, options.target_branch)
5204 refspec = '%s:refs/for/%s%s' % (new_upload.commit_to_push, branch,
5205 refspec_suffix)
Joanna Wangc710e2d2023-01-25 14:53:22 +00005206
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005207 # Git push
5208 git_push_metadata = {
5209 'gerrit_host':
5210 cl.GetGerritHost(),
5211 'title':
5212 options.title or '<untitled>',
5213 'change_id':
5214 git_footers.get_footer_change_id(new_upload.change_desc.description),
5215 'description':
5216 new_upload.change_desc.description,
5217 }
5218 push_stdout = cl._RunGitPushWithTraces(refspec, refspec_opts,
5219 git_push_metadata,
5220 options.push_options)
Joanna Wangc710e2d2023-01-25 14:53:22 +00005221
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005222 # Post push updates
5223 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
5224 change_numbers = [
5225 m.group(1) for m in map(regex.match, push_stdout.splitlines()) if m
5226 ]
Joanna Wangc710e2d2023-01-25 14:53:22 +00005227
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005228 for i, (cl, new_upload) in enumerate(uploads_by_cl):
5229 cl.PostUploadUpdates(options, new_upload, change_numbers[i])
Joanna Wangc710e2d2023-01-25 14:53:22 +00005230
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005231 return 0
Joanna Wang18de1f62023-01-21 01:24:24 +00005232
5233
5234def _UploadAllPrecheck(options, orig_args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005235 # type: (optparse.Values, Sequence[str]) -> Tuple[Sequence[Changelist],
5236 # bool]
5237 """Checks the state of the tree and gives the user uploading options
Joanna Wang18de1f62023-01-21 01:24:24 +00005238
5239 Returns: A tuple of the ordered list of changes that have new commits
5240 since their last upload and a boolean of whether the user wants to
5241 cherry-pick and upload the current branch instead of uploading all cls.
5242 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005243 cl = Changelist()
5244 if cl.GetBranch() is None:
5245 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
Joanna Wang6b98cdc2023-02-16 00:37:20 +00005246
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005247 branch_ref = None
5248 cls = []
5249 must_upload_upstream = False
5250 first_pass = True
Joanna Wang18de1f62023-01-21 01:24:24 +00005251
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005252 Changelist._GerritCommitMsgHookCheck(offer_removal=not options.force)
Joanna Wang18de1f62023-01-21 01:24:24 +00005253
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005254 while True:
5255 if len(cls) > _MAX_STACKED_BRANCHES_UPLOAD:
5256 DieWithError(
5257 'More than %s branches in the stack have not been uploaded.\n'
5258 'Are your branches in a misconfigured state?\n'
5259 'If not, please upload some upstream changes first.' %
5260 (_MAX_STACKED_BRANCHES_UPLOAD))
Joanna Wang18de1f62023-01-21 01:24:24 +00005261
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005262 cl = Changelist(branchref=branch_ref)
Joanna Wang18de1f62023-01-21 01:24:24 +00005263
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005264 # Only add CL if it has anything to commit.
5265 base_commit = cl.GetCommonAncestorWithUpstream()
5266 end_commit = RunGit(['rev-parse', cl.GetBranchRef()]).strip()
Joanna Wang6215dd02023-02-07 15:58:03 +00005267
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005268 commit_summary = _GetCommitCountSummary(base_commit, end_commit)
5269 if commit_summary:
5270 cls.append(cl)
5271 if (not first_pass and
5272 cl._GitGetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY)
5273 is None):
5274 # We are mid-stack and the user must upload their upstream
5275 # branches.
5276 must_upload_upstream = True
5277 print(f'Found change with {commit_summary}...')
5278 elif first_pass: # The current branch has nothing to commit. Exit.
5279 DieWithError('Branch %s has nothing to commit' % cl.GetBranch())
5280 # Else: A mid-stack branch has nothing to commit. We do not add it to
5281 # cls.
5282 first_pass = False
Joanna Wang6215dd02023-02-07 15:58:03 +00005283
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005284 # Cases below determine if we should continue to traverse up the tree.
5285 origin, upstream_branch_ref = Changelist.FetchUpstreamTuple(
5286 cl.GetBranch())
5287 branch_ref = upstream_branch_ref # set branch for next run.
Joanna Wang18de1f62023-01-21 01:24:24 +00005288
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005289 upstream_branch = scm.GIT.ShortBranchName(upstream_branch_ref)
5290 upstream_last_upload = scm.GIT.GetBranchConfig(
5291 settings.GetRoot(), upstream_branch, LAST_UPLOAD_HASH_CONFIG_KEY)
Joanna Wang6215dd02023-02-07 15:58:03 +00005292
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005293 # Case 1: We've reached the beginning of the tree.
5294 if origin != '.':
5295 break
Joanna Wang18de1f62023-01-21 01:24:24 +00005296
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005297 # Case 2: If any upstream branches have never been uploaded,
5298 # the user MUST upload them unless they are empty. Continue to
5299 # next loop to add upstream if it is not empty.
5300 if not upstream_last_upload:
5301 continue
Joanna Wang18de1f62023-01-21 01:24:24 +00005302
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005303 # Case 3: If upstream's last_upload == cl.base_commit we do
5304 # not need to upload any more upstreams from this point on.
5305 # (Even if there may be diverged branches higher up the tree)
5306 if base_commit == upstream_last_upload:
5307 break
Joanna Wang18de1f62023-01-21 01:24:24 +00005308
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005309 # Case 4: If upstream's last_upload < cl.base_commit we are
5310 # uploading cl and upstream_cl.
5311 # Continue up the tree to check other branch relations.
5312 if scm.GIT.IsAncestor(upstream_last_upload, base_commit):
5313 continue
Joanna Wang18de1f62023-01-21 01:24:24 +00005314
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005315 # Case 5: If cl.base_commit < upstream's last_upload the user
5316 # must rebase before uploading.
5317 if scm.GIT.IsAncestor(base_commit, upstream_last_upload):
5318 DieWithError(
5319 'At least one branch in the stack has diverged from its upstream '
5320 'branch and does not contain its upstream\'s last upload.\n'
5321 'Please rebase the stack with `git rebase-update` before uploading.'
5322 )
Joanna Wang18de1f62023-01-21 01:24:24 +00005323
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005324 # The tree went through a rebase. LAST_UPLOAD_HASH_CONFIG_KEY no longer
5325 # has any relation to commits in the tree. Continue up the tree until we
5326 # hit the root.
Joanna Wang18de1f62023-01-21 01:24:24 +00005327
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005328 # We assume all cls in the stack have the same auth requirements and only
5329 # check this once.
5330 cls[0].EnsureAuthenticated(force=options.force)
Joanna Wang18de1f62023-01-21 01:24:24 +00005331
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005332 cherry_pick = False
5333 if len(cls) > 1:
5334 opt_message = ''
5335 branches = ', '.join([cl.branch for cl in cls])
5336 if len(orig_args):
5337 opt_message = ('options %s will be used for all uploads.\n' %
5338 orig_args)
5339 if must_upload_upstream:
5340 msg = ('At least one parent branch in `%s` has never been uploaded '
5341 'and must be uploaded before/with `%s`.\n' %
5342 (branches, cls[1].branch))
5343 if options.cherry_pick_stacked:
5344 DieWithError(msg)
5345 if not options.force:
5346 confirm_or_exit('\n' + opt_message + msg)
5347 else:
5348 if options.cherry_pick_stacked:
5349 print('cherry-picking `%s` on %s\'s last upload' %
5350 (cls[0].branch, cls[1].branch))
5351 cherry_pick = True
5352 elif not options.force:
5353 answer = gclient_utils.AskForData(
5354 '\n' + opt_message +
5355 'Press enter to update branches %s.\nOr type `n` to upload only '
5356 '`%s` cherry-picked on %s\'s last upload:' %
5357 (branches, cls[0].branch, cls[1].branch))
5358 if answer.lower() == 'n':
5359 cherry_pick = True
5360 return cls, cherry_pick
Joanna Wang18de1f62023-01-21 01:24:24 +00005361
5362
Francois Dorayd42c6812017-05-30 15:10:20 -04005363@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005364@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04005365def CMDsplit(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005366 """Splits a branch into smaller branches and uploads CLs.
Francois Dorayd42c6812017-05-30 15:10:20 -04005367
5368 Creates a branch and uploads a CL for each group of files modified in the
5369 current branch that share a common OWNERS file. In the CL description and
Edward Lemurac5c55f2020-02-29 00:17:16 +00005370 comment, the string '$directory', is replaced with the directory containing
5371 the shared OWNERS file.
Francois Dorayd42c6812017-05-30 15:10:20 -04005372 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005373 parser.add_option('-d',
5374 '--description',
5375 dest='description_file',
5376 help='A text file containing a CL description in which '
5377 '$directory will be replaced by each CL\'s directory.')
5378 parser.add_option('-c',
5379 '--comment',
5380 dest='comment_file',
5381 help='A text file containing a CL comment.')
5382 parser.add_option(
5383 '-n',
5384 '--dry-run',
5385 dest='dry_run',
5386 action='store_true',
5387 default=False,
5388 help='List the files and reviewers for each CL that would '
5389 'be created, but don\'t create branches or CLs.')
5390 parser.add_option('--cq-dry-run',
5391 action='store_true',
5392 help='If set, will do a cq dry run for each uploaded CL. '
5393 'Please be careful when doing this; more than ~10 CLs '
5394 'has the potential to overload our build '
5395 'infrastructure. Try to upload these not during high '
5396 'load times (usually 11-3 Mountain View time). Email '
5397 'infra-dev@chromium.org with any questions.')
5398 parser.add_option(
5399 '-a',
5400 '--enable-auto-submit',
5401 action='store_true',
5402 dest='enable_auto_submit',
5403 default=True,
5404 help='Sends your change to the CQ after an approval. Only '
5405 'works on repos that have the Auto-Submit label '
5406 'enabled')
5407 parser.add_option(
5408 '--disable-auto-submit',
5409 action='store_false',
5410 dest='enable_auto_submit',
5411 help='Disables automatic sending of the changes to the CQ '
5412 'after approval. Note that auto-submit only works for '
5413 'repos that have the Auto-Submit label enabled.')
5414 parser.add_option('--max-depth',
5415 type='int',
5416 default=0,
5417 help='The max depth to look for OWNERS files. Useful for '
5418 'controlling the granularity of the split CLs, e.g. '
5419 '--max-depth=1 will only split by top-level '
5420 'directory. Specifying a value less than 1 means no '
5421 'limit on max depth.')
5422 parser.add_option('--topic',
5423 default=None,
5424 help='Topic to specify when uploading')
5425 options, _ = parser.parse_args(args)
Francois Dorayd42c6812017-05-30 15:10:20 -04005426
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005427 if not options.description_file:
5428 parser.error('No --description flag specified.')
Francois Dorayd42c6812017-05-30 15:10:20 -04005429
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005430 def WrappedCMDupload(args):
5431 return CMDupload(OptionParser(), args)
Francois Dorayd42c6812017-05-30 15:10:20 -04005432
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005433 return split_cl.SplitCl(options.description_file, options.comment_file,
5434 Changelist, WrappedCMDupload, options.dry_run,
5435 options.cq_dry_run, options.enable_auto_submit,
5436 options.max_depth, options.topic,
5437 settings.GetRoot())
Francois Dorayd42c6812017-05-30 15:10:20 -04005438
5439
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005440@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005441@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005442def CMDdcommit(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005443 """DEPRECATED: Used to commit the current changelist via git-svn."""
5444 message = ('git-cl no longer supports committing to SVN repositories via '
5445 'git-svn. You probably want to use `git cl land` instead.')
5446 print(message)
5447 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005448
5449
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005450@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005451@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00005452def CMDland(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005453 """Commits the current changelist via git.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005454
5455 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
5456 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005457 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005458 parser.add_option('--bypass-hooks',
5459 action='store_true',
5460 dest='bypass_hooks',
5461 help='bypass upload presubmit hook')
5462 parser.add_option('-f',
5463 '--force',
5464 action='store_true',
5465 dest='force',
5466 help="force yes to questions (don't prompt)")
5467 parser.add_option(
5468 '--parallel',
5469 action='store_true',
5470 help='Run all tests specified by input_api.RunTests in all '
5471 'PRESUBMIT files in parallel.')
5472 parser.add_option('--resultdb',
5473 action='store_true',
5474 help='Run presubmit checks in the ResultSink environment '
5475 'and send results to the ResultDB database.')
5476 parser.add_option('--realm', help='LUCI realm if reporting to ResultDB')
5477 (options, args) = parser.parse_args(args)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005478
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005479 cl = Changelist()
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005480
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005481 if not cl.GetIssue():
5482 DieWithError('You must upload the change first to Gerrit.\n'
5483 ' If you would rather have `git cl land` upload '
5484 'automatically for you, see http://crbug.com/642759')
5485 return cl.CMDLand(options.force, options.bypass_hooks, options.verbose,
5486 options.parallel, options.resultdb, options.realm)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005487
5488
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005489@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005490@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005491def CMDpatch(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005492 """Applies (cherry-picks) a Gerrit changelist locally."""
5493 parser.add_option('-b',
5494 dest='newbranch',
5495 help='create a new branch off trunk for the patch')
5496 parser.add_option('-f',
5497 '--force',
5498 action='store_true',
5499 help='overwrite state on the current or chosen branch')
5500 parser.add_option('-n',
5501 '--no-commit',
5502 action='store_true',
5503 dest='nocommit',
5504 help='don\'t commit after patch applies.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005505
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005506 group = optparse.OptionGroup(
5507 parser,
5508 'Options for continuing work on the current issue uploaded from a '
5509 'different clone (e.g. different machine). Must be used independently '
5510 'from the other options. No issue number should be specified, and the '
5511 'branch must have an issue number associated with it')
5512 group.add_option('--reapply',
5513 action='store_true',
5514 dest='reapply',
5515 help='Reset the branch and reapply the issue.\n'
5516 'CAUTION: This will undo any local changes in this '
5517 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005518
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005519 group.add_option('--pull',
5520 action='store_true',
5521 dest='pull',
5522 help='Performs a pull before reapplying.')
5523 parser.add_option_group(group)
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005524
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005525 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005526
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005527 if options.reapply:
5528 if options.newbranch:
5529 parser.error('--reapply works on the current branch only.')
5530 if len(args) > 0:
5531 parser.error('--reapply implies no additional arguments.')
5532
5533 cl = Changelist()
5534 if not cl.GetIssue():
5535 parser.error('Current branch must have an associated issue.')
5536
5537 upstream = cl.GetUpstreamBranch()
5538 if upstream is None:
5539 parser.error('No upstream branch specified. Cannot reset branch.')
5540
5541 RunGit(['reset', '--hard', upstream])
5542 if options.pull:
5543 RunGit(['pull'])
5544
5545 target_issue_arg = ParseIssueNumberArgument(cl.GetIssue())
5546 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.nocommit,
5547 options.force, False)
5548
5549 if len(args) != 1 or not args[0]:
5550 parser.error('Must specify issue number or URL.')
5551
5552 target_issue_arg = ParseIssueNumberArgument(args[0])
5553 if not target_issue_arg.valid:
5554 parser.error('Invalid issue ID or URL.')
5555
5556 # We don't want uncommitted changes mixed up with the patch.
5557 if git_common.is_dirty_git_tree('patch'):
5558 return 1
5559
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005560 if options.newbranch:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005561 if options.force:
5562 RunGit(['branch', '-D', options.newbranch],
5563 stderr=subprocess2.PIPE,
5564 error_ok=True)
5565 git_new_branch.create_new_branch(options.newbranch)
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005566
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005567 cl = Changelist(codereview_host=target_issue_arg.hostname,
5568 issue=target_issue_arg.issue)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005569
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005570 if not args[0].isdigit():
5571 print('canonical issue/change URL: %s\n' % cl.GetIssueURL())
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005572
Joanna Wang44e9bee2023-01-25 21:51:42 +00005573 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.nocommit,
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005574 options.force, options.newbranch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005575
5576
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005577def GetTreeStatus(url=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005578 """Fetches the tree status and returns either 'open', 'closed',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005579 'unknown' or 'unset'."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005580 url = url or settings.GetTreeStatusUrl(error_ok=True)
5581 if url:
5582 status = str(urllib.request.urlopen(url).read().lower())
5583 if status.find('closed') != -1 or status == '0':
5584 return 'closed'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00005585
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005586 if status.find('open') != -1 or status == '1':
5587 return 'open'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00005588
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005589 return 'unknown'
5590 return 'unset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005591
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005592
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005593def GetTreeStatusReason():
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005594 """Fetches the tree status from a json url and returns the message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005595 with the reason for the tree to be opened or closed."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005596 url = settings.GetTreeStatusUrl()
5597 json_url = urllib.parse.urljoin(url, '/current?format=json')
5598 connection = urllib.request.urlopen(json_url)
5599 status = json.loads(connection.read())
5600 connection.close()
5601 return status['message']
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005602
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005603
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005604@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005605def CMDtree(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005606 """Shows the status of the tree."""
5607 _, args = parser.parse_args(args)
5608 status = GetTreeStatus()
5609 if 'unset' == status:
5610 print(
5611 'You must configure your tree status URL by running "git cl config".'
5612 )
5613 return 2
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005614
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005615 print('The tree is %s' % status)
5616 print()
5617 print(GetTreeStatusReason())
5618 if status != 'open':
5619 return 1
5620 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005621
5622
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005623@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00005624def CMDtry(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005625 """Triggers tryjobs using either Buildbucket or CQ dry run."""
5626 group = optparse.OptionGroup(parser, 'Tryjob options')
5627 group.add_option(
5628 '-b',
5629 '--bot',
5630 action='append',
5631 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5632 'times to specify multiple builders. ex: '
5633 '"-b win_rel -b win_layout". See '
5634 'the try server waterfall for the builders name and the tests '
5635 'available.'))
5636 group.add_option(
5637 '-B',
5638 '--bucket',
5639 default='',
5640 help=('Buildbucket bucket to send the try requests. Format: '
5641 '"luci.$LUCI_PROJECT.$LUCI_BUCKET". eg: "luci.chromium.try"'))
5642 group.add_option(
5643 '-r',
5644 '--revision',
5645 help='Revision to use for the tryjob; default: the revision will '
5646 'be determined by the try recipe that builder runs, which usually '
5647 'defaults to HEAD of origin/master or origin/main')
5648 group.add_option(
5649 '-c',
5650 '--clobber',
5651 action='store_true',
5652 default=False,
5653 help='Force a clobber before building; that is don\'t do an '
5654 'incremental build')
5655 group.add_option('--category',
5656 default='git_cl_try',
5657 help='Specify custom build category.')
5658 group.add_option(
5659 '--project',
5660 help='Override which project to use. Projects are defined '
5661 'in recipe to determine to which repository or directory to '
5662 'apply the patch')
5663 group.add_option(
5664 '-p',
5665 '--property',
5666 dest='properties',
5667 action='append',
5668 default=[],
5669 help='Specify generic properties in the form -p key1=value1 -p '
5670 'key2=value2 etc. The value will be treated as '
5671 'json if decodable, or as string otherwise. '
5672 'NOTE: using this may make your tryjob not usable for CQ, '
5673 'which will then schedule another tryjob with default properties')
5674 group.add_option('--buildbucket-host',
5675 default='cr-buildbucket.appspot.com',
5676 help='Host of buildbucket. The default host is %default.')
5677 parser.add_option_group(group)
5678 parser.add_option('-R',
5679 '--retry-failed',
5680 action='store_true',
5681 default=False,
5682 help='Retry failed jobs from the latest set of tryjobs. '
5683 'Not allowed with --bucket and --bot options.')
5684 parser.add_option(
5685 '-i',
5686 '--issue',
5687 type=int,
5688 help='Operate on this issue instead of the current branch\'s implicit '
5689 'issue.')
5690 options, args = parser.parse_args(args)
maruel@chromium.org15192402012-09-06 12:38:29 +00005691
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005692 # Make sure that all properties are prop=value pairs.
5693 bad_params = [x for x in options.properties if '=' not in x]
5694 if bad_params:
5695 parser.error('Got properties with missing "=": %s' % bad_params)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005696
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005697 if args:
5698 parser.error('Unknown arguments: %s' % args)
maruel@chromium.org15192402012-09-06 12:38:29 +00005699
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005700 cl = Changelist(issue=options.issue)
5701 if not cl.GetIssue():
5702 parser.error('Need to upload first.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005703
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005704 # HACK: warm up Gerrit change detail cache to save on RPCs.
5705 cl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005706
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005707 error_message = cl.CannotTriggerTryJobReason()
5708 if error_message:
5709 parser.error('Can\'t trigger tryjobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005710
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005711 if options.bot:
5712 if options.retry_failed:
5713 parser.error('--bot is not compatible with --retry-failed.')
5714 if not options.bucket:
5715 parser.error('A bucket (e.g. "chromium/try") is required.')
Edward Lemur45768512020-03-02 19:03:14 +00005716
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005717 triggered = [b for b in options.bot if 'triggered' in b]
5718 if triggered:
5719 parser.error(
5720 'Cannot schedule builds on triggered bots: %s.\n'
5721 'This type of bot requires an initial job from a parent (usually a '
5722 'builder). Schedule a job on the parent instead.\n' % triggered)
Edward Lemur45768512020-03-02 19:03:14 +00005723
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005724 if options.bucket.startswith('.master'):
5725 parser.error('Buildbot masters are not supported.')
Edward Lemur45768512020-03-02 19:03:14 +00005726
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005727 project, bucket = _parse_bucket(options.bucket)
5728 if project is None or bucket is None:
5729 parser.error('Invalid bucket: %s.' % options.bucket)
5730 jobs = sorted((project, bucket, bot) for bot in options.bot)
5731 elif options.retry_failed:
5732 print('Searching for failed tryjobs...')
5733 builds, patchset = _fetch_latest_builds(cl, DEFAULT_BUILDBUCKET_HOST)
5734 if options.verbose:
5735 print('Got %d builds in patchset #%d' % (len(builds), patchset))
5736 jobs = _filter_failed_for_retry(builds)
5737 if not jobs:
5738 print('There are no failed jobs in the latest set of jobs '
5739 '(patchset #%d), doing nothing.' % patchset)
5740 return 0
5741 num_builders = len(jobs)
5742 if num_builders > 10:
5743 confirm_or_exit('There are %d builders with failed builds.' %
5744 num_builders,
5745 action='continue')
5746 else:
5747 if options.verbose:
5748 print('git cl try with no bots now defaults to CQ dry run.')
5749 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5750 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005751
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005752 patchset = cl.GetMostRecentPatchset()
5753 try:
5754 _trigger_tryjobs(cl, jobs, options, patchset)
5755 except BuildbucketResponseException as ex:
5756 print('ERROR: %s' % ex)
5757 return 1
5758 return 0
maruel@chromium.org15192402012-09-06 12:38:29 +00005759
5760
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005761@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005762def CMDtry_results(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005763 """Prints info about results for tryjobs associated with the current CL."""
5764 group = optparse.OptionGroup(parser, 'Tryjob results options')
5765 group.add_option('-p',
5766 '--patchset',
5767 type=int,
5768 help='patchset number if not current.')
5769 group.add_option('--print-master',
5770 action='store_true',
5771 help='print master name as well.')
5772 group.add_option('--color',
5773 action='store_true',
5774 default=setup_color.IS_TTY,
5775 help='force color output, useful when piping output.')
5776 group.add_option('--buildbucket-host',
5777 default='cr-buildbucket.appspot.com',
5778 help='Host of buildbucket. The default host is %default.')
5779 group.add_option(
5780 '--json',
5781 help=('Path of JSON output file to write tryjob results to,'
5782 'or "-" for stdout.'))
5783 parser.add_option_group(group)
5784 parser.add_option(
5785 '-i',
5786 '--issue',
5787 type=int,
5788 help='Operate on this issue instead of the current branch\'s implicit '
5789 'issue.')
5790 options, args = parser.parse_args(args)
5791 if args:
5792 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005793
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005794 cl = Changelist(issue=options.issue)
5795 if not cl.GetIssue():
5796 parser.error('Need to upload first.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005797
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005798 patchset = options.patchset
tandrii221ab252016-10-06 08:12:04 -07005799 if not patchset:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005800 patchset = cl.GetMostRecentDryRunPatchset()
5801 if not patchset:
5802 parser.error('Code review host doesn\'t know about issue %s. '
5803 'No access to issue or wrong issue number?\n'
5804 'Either upload first, or pass --patchset explicitly.' %
5805 cl.GetIssue())
tandrii221ab252016-10-06 08:12:04 -07005806
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005807 try:
5808 jobs = _fetch_tryjobs(cl, DEFAULT_BUILDBUCKET_HOST, patchset)
5809 except BuildbucketResponseException as ex:
5810 print('Buildbucket error: %s' % ex)
5811 return 1
5812 if options.json:
5813 write_json(options.json, jobs)
5814 else:
5815 _print_tryjobs(options, jobs)
5816 return 0
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005817
5818
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005819@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005820@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005821def CMDupstream(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005822 """Prints or sets the name of the upstream branch, if any."""
5823 _, args = parser.parse_args(args)
5824 if len(args) > 1:
5825 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005826
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005827 cl = Changelist()
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005828 if args:
5829 # One arg means set upstream branch.
5830 branch = cl.GetBranch()
5831 RunGit(['branch', '--set-upstream-to', args[0], branch])
5832 cl = Changelist()
5833 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(), ))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005834
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005835 # Clear configured merge-base, if there is one.
5836 git_common.remove_merge_base(branch)
5837 else:
5838 print(cl.GetUpstreamBranch())
5839 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005840
5841
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005842@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00005843def CMDweb(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005844 """Opens the current CL in the web browser."""
5845 parser.add_option('-p',
5846 '--print-only',
5847 action='store_true',
5848 dest='print_only',
5849 help='Only print the Gerrit URL, don\'t open it in the '
5850 'browser.')
5851 (options, args) = parser.parse_args(args)
5852 if args:
5853 parser.error('Unrecognized args: %s' % ' '.join(args))
thestig@chromium.org00858c82013-12-02 23:08:03 +00005854
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005855 issue_url = Changelist().GetIssueURL()
5856 if not issue_url:
5857 print('ERROR No issue to open', file=sys.stderr)
5858 return 1
thestig@chromium.org00858c82013-12-02 23:08:03 +00005859
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005860 if options.print_only:
5861 print(issue_url)
5862 return 0
5863
5864 # Redirect I/O before invoking browser to hide its output. For example, this
5865 # allows us to hide the "Created new window in existing browser session."
5866 # message from Chrome. Based on https://stackoverflow.com/a/2323563.
5867 saved_stdout = os.dup(1)
5868 saved_stderr = os.dup(2)
5869 os.close(1)
5870 os.close(2)
5871 os.open(os.devnull, os.O_RDWR)
5872 try:
5873 webbrowser.open(issue_url)
5874 finally:
5875 os.dup2(saved_stdout, 1)
5876 os.dup2(saved_stderr, 2)
Orr Bernstein0b960582022-12-22 20:16:18 +00005877 return 0
5878
thestig@chromium.org00858c82013-12-02 23:08:03 +00005879
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005880@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005881def CMDset_commit(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005882 """Sets the commit bit to trigger the CQ."""
5883 parser.add_option('-d',
5884 '--dry-run',
5885 action='store_true',
5886 help='trigger in dry run mode')
5887 parser.add_option('-c',
5888 '--clear',
5889 action='store_true',
5890 help='stop CQ run, if any')
5891 parser.add_option(
5892 '-i',
5893 '--issue',
5894 type=int,
5895 help='Operate on this issue instead of the current branch\'s implicit '
5896 'issue.')
5897 options, args = parser.parse_args(args)
5898 if args:
5899 parser.error('Unrecognized args: %s' % ' '.join(args))
5900 if [options.dry_run, options.clear].count(True) > 1:
5901 parser.error('Only one of --dry-run, and --clear are allowed.')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005902
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005903 cl = Changelist(issue=options.issue)
5904 if not cl.GetIssue():
5905 parser.error('Must upload the issue first.')
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00005906
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005907 if options.clear:
5908 state = _CQState.NONE
5909 elif options.dry_run:
5910 state = _CQState.DRY_RUN
5911 else:
5912 state = _CQState.COMMIT
5913 cl.SetCQState(state)
5914 return 0
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005915
5916
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005917@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005918def CMDset_close(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005919 """Closes the issue."""
5920 parser.add_option(
5921 '-i',
5922 '--issue',
5923 type=int,
5924 help='Operate on this issue instead of the current branch\'s implicit '
5925 'issue.')
5926 options, args = parser.parse_args(args)
5927 if args:
5928 parser.error('Unrecognized args: %s' % ' '.join(args))
5929 cl = Changelist(issue=options.issue)
5930 # Ensure there actually is an issue to close.
5931 if not cl.GetIssue():
5932 DieWithError('ERROR: No issue to close.')
5933 cl.CloseIssue()
5934 return 0
groby@chromium.org411034a2013-02-26 15:12:01 +00005935
5936
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005937@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005938def CMDdiff(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005939 """Shows differences between local tree and last upload."""
5940 parser.add_option('--stat',
5941 action='store_true',
5942 dest='stat',
5943 help='Generate a diffstat')
5944 options, args = parser.parse_args(args)
5945 if args:
5946 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005947
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005948 cl = Changelist()
5949 issue = cl.GetIssue()
5950 branch = cl.GetBranch()
5951 if not issue:
5952 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005953
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005954 base = cl._GitGetBranchConfigValue(LAST_UPLOAD_HASH_CONFIG_KEY)
5955 if not base:
5956 base = cl._GitGetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY)
5957 if not base:
5958 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5959 revision_info = detail['revisions'][detail['current_revision']]
5960 fetch_info = revision_info['fetch']['http']
5961 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5962 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005963
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005964 cmd = ['git', 'diff']
5965 if options.stat:
5966 cmd.append('--stat')
5967 cmd.append(base)
5968 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005969
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005970 return 0
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005971
5972
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005973@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005974def CMDowners(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005975 """Finds potential owners for reviewing."""
5976 parser.add_option(
5977 '--ignore-current',
5978 action='store_true',
5979 help='Ignore the CL\'s current reviewers and start from scratch.')
5980 parser.add_option('--ignore-self',
5981 action='store_true',
5982 help='Do not consider CL\'s author as an owners.')
5983 parser.add_option('--no-color',
5984 action='store_true',
5985 help='Use this option to disable color output')
5986 parser.add_option('--batch',
5987 action='store_true',
5988 help='Do not run interactively, just suggest some')
5989 # TODO: Consider moving this to another command, since other
5990 # git-cl owners commands deal with owners for a given CL.
5991 parser.add_option('--show-all',
5992 action='store_true',
5993 help='Show all owners for a particular file')
5994 options, args = parser.parse_args(args)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005995
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005996 cl = Changelist()
5997 author = cl.GetAuthor()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005998
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005999 if options.show_all:
6000 if len(args) == 0:
6001 print('No files specified for --show-all. Nothing to do.')
6002 return 0
6003 owners_by_path = cl.owners_client.BatchListOwners(args)
6004 for path in args:
6005 print('Owners for %s:' % path)
6006 print('\n'.join(
6007 ' - %s' % owner
6008 for owner in owners_by_path.get(path, ['No owners found'])))
6009 return 0
Yang Guo6e269a02019-06-26 11:17:02 +00006010
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006011 if args:
6012 if len(args) > 1:
6013 parser.error('Unknown args.')
6014 base_branch = args[0]
6015 else:
6016 # Default to diffing against the common ancestor of the upstream branch.
6017 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00006018
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006019 affected_files = cl.GetAffectedFiles(base_branch)
Dirk Prankebf980882017-09-02 15:08:00 -07006020
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006021 if options.batch:
6022 owners = cl.owners_client.SuggestOwners(affected_files,
6023 exclude=[author])
6024 print('\n'.join(owners))
6025 return 0
Dirk Prankebf980882017-09-02 15:08:00 -07006026
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006027 return owners_finder.OwnersFinder(
6028 affected_files,
6029 author, [] if options.ignore_current else cl.GetReviewers(),
6030 cl.owners_client,
6031 disable_color=options.no_color,
6032 ignore_author=options.ignore_self).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00006033
6034
Aiden Bennerc08566e2018-10-03 17:52:42 +00006035def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006036 """Generates a diff command."""
6037 # Generate diff for the current branch's changes.
6038 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
Aiden Bennerc08566e2018-10-03 17:52:42 +00006039
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006040 if allow_prefix:
6041 # explicitly setting --src-prefix and --dst-prefix is necessary in the
6042 # case that diff.noprefix is set in the user's git config.
6043 diff_cmd += ['--src-prefix=a/', '--dst-prefix=b/']
6044 else:
6045 diff_cmd += ['--no-prefix']
Aiden Bennerc08566e2018-10-03 17:52:42 +00006046
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006047 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006048
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006049 if args:
6050 for arg in args:
6051 if os.path.isdir(arg) or os.path.isfile(arg):
6052 diff_cmd.append(arg)
6053 else:
6054 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006055
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006056 return diff_cmd
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006057
Andrii Shyshkalov18975322017-01-25 16:44:13 +01006058
Jamie Madill5e96ad12020-01-13 16:08:35 +00006059def _RunClangFormatDiff(opts, clang_diff_files, top_dir, upstream_commit):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006060 """Runs clang-format-diff and sets a return value if necessary."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006061 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
6062 # formatted. This is used to block during the presubmit.
6063 return_value = 0
Jamie Madill5e96ad12020-01-13 16:08:35 +00006064
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006065 # Locate the clang-format binary in the checkout
Jamie Madill5e96ad12020-01-13 16:08:35 +00006066 try:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006067 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
Jamie Madill5e96ad12020-01-13 16:08:35 +00006068 except clang_format.NotFoundError as e:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006069 DieWithError(e)
Jamie Madill5e96ad12020-01-13 16:08:35 +00006070
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006071 if opts.full or settings.GetFormatFullByDefault():
6072 cmd = [clang_format_tool]
6073 if not opts.dry_run and not opts.diff:
6074 cmd.append('-i')
6075 if opts.dry_run:
6076 for diff_file in clang_diff_files:
6077 with open(diff_file, 'r') as myfile:
6078 code = myfile.read().replace('\r\n', '\n')
6079 stdout = RunCommand(cmd + [diff_file], cwd=top_dir)
6080 stdout = stdout.replace('\r\n', '\n')
6081 if opts.diff:
6082 sys.stdout.write(stdout)
6083 if code != stdout:
6084 return_value = 2
6085 else:
6086 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
6087 if opts.diff:
6088 sys.stdout.write(stdout)
6089 else:
6090 try:
6091 script = clang_format.FindClangFormatScriptInChromiumTree(
6092 'clang-format-diff.py')
6093 except clang_format.NotFoundError as e:
6094 DieWithError(e)
Jamie Madill5e96ad12020-01-13 16:08:35 +00006095
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006096 cmd = ['vpython3', script, '-p0']
6097 if not opts.dry_run and not opts.diff:
6098 cmd.append('-i')
Jamie Madill5e96ad12020-01-13 16:08:35 +00006099
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006100 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
6101 diff_output = RunGit(diff_cmd).encode('utf-8')
Jamie Madill5e96ad12020-01-13 16:08:35 +00006102
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006103 env = os.environ.copy()
6104 env['PATH'] = (str(os.path.dirname(clang_format_tool)) + os.pathsep +
6105 env['PATH'])
6106 stdout = RunCommand(cmd,
6107 stdin=diff_output,
6108 cwd=top_dir,
6109 env=env,
6110 shell=sys.platform.startswith('win32'))
6111 if opts.diff:
6112 sys.stdout.write(stdout)
6113 if opts.dry_run and len(stdout) > 0:
6114 return_value = 2
6115
6116 return return_value
Jamie Madill5e96ad12020-01-13 16:08:35 +00006117
6118
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006119def _RunRustFmt(opts, rust_diff_files, top_dir, upstream_commit):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006120 """Runs rustfmt. Just like _RunClangFormatDiff returns 2 to indicate that
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006121 presubmit checks have failed (and returns 0 otherwise)."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006122 # Locate the rustfmt binary.
6123 try:
6124 rustfmt_tool = rustfmt.FindRustfmtToolInChromiumTree()
6125 except rustfmt.NotFoundError as e:
6126 DieWithError(e)
6127
6128 # TODO(crbug.com/1440869): Support formatting only the changed lines
6129 # if `opts.full or settings.GetFormatFullByDefault()` is False.
6130 cmd = [rustfmt_tool]
6131 if opts.dry_run:
6132 cmd.append('--check')
6133 cmd += rust_diff_files
6134 rustfmt_exitcode = subprocess2.call(cmd)
6135
6136 if opts.presubmit and rustfmt_exitcode != 0:
6137 return 2
6138
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006139 return 0
6140
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006141
Olivier Robin0a6b5442022-04-07 07:25:04 +00006142def _RunSwiftFormat(opts, swift_diff_files, top_dir, upstream_commit):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006143 """Runs swift-format. Just like _RunClangFormatDiff returns 2 to indicate
Olivier Robin0a6b5442022-04-07 07:25:04 +00006144 that presubmit checks have failed (and returns 0 otherwise)."""
Andrew Grievecca48db2023-09-14 14:12:23 +00006145 if sys.platform != 'darwin':
6146 DieWithError('swift-format is only supported on macOS.')
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006147 # Locate the swift-format binary.
6148 try:
6149 swift_format_tool = swift_format.FindSwiftFormatToolInChromiumTree()
6150 except swift_format.NotFoundError as e:
6151 DieWithError(e)
6152
6153 cmd = [swift_format_tool]
6154 if opts.dry_run:
6155 cmd += ['lint', '-s']
6156 else:
6157 cmd += ['format', '-i']
6158 cmd += swift_diff_files
6159 swift_format_exitcode = subprocess2.call(cmd)
6160
6161 if opts.presubmit and swift_format_exitcode != 0:
6162 return 2
6163
Olivier Robin0a6b5442022-04-07 07:25:04 +00006164 return 0
6165
Olivier Robin0a6b5442022-04-07 07:25:04 +00006166
Andrew Grievecca48db2023-09-14 14:12:23 +00006167def _RunYapf(opts, paths, top_dir, upstream_commit):
6168 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
6169 yapf_tool = os.path.join(depot_tools_path, 'yapf')
6170
6171 # Used for caching.
6172 yapf_configs = {}
6173 for p in paths:
6174 # Find the yapf style config for the current file, defaults to depot
6175 # tools default.
6176 _FindYapfConfigFile(p, yapf_configs, top_dir)
6177
6178 # Turn on python formatting by default if a yapf config is specified.
6179 # This breaks in the case of this repo though since the specified
6180 # style file is also the global default.
6181 if opts.python is None:
6182 paths = [
6183 p for p in paths
6184 if _FindYapfConfigFile(p, yapf_configs, top_dir) is not None
6185 ]
6186
6187 # Note: yapf still seems to fix indentation of the entire file
6188 # even if line ranges are specified.
6189 # See https://github.com/google/yapf/issues/499
6190 if not opts.full and paths:
6191 py_line_diffs = _ComputeDiffLineRanges(paths, upstream_commit)
6192
6193 yapfignore_patterns = _GetYapfIgnorePatterns(top_dir)
6194 paths = _FilterYapfIgnoredFiles(paths, yapfignore_patterns)
6195
6196 return_value = 0
6197 for path in paths:
6198 yapf_style = _FindYapfConfigFile(path, yapf_configs, top_dir)
6199 # Default to pep8 if not .style.yapf is found.
6200 if not yapf_style:
6201 yapf_style = 'pep8'
6202
6203 with open(path, 'r') as py_f:
6204 if 'python2' in py_f.readline():
6205 vpython_script = 'vpython'
6206 else:
6207 vpython_script = 'vpython3'
6208
6209 cmd = [vpython_script, yapf_tool, '--style', yapf_style, path]
6210
6211 has_formattable_lines = False
6212 if not opts.full:
6213 # Only run yapf over changed line ranges.
6214 for diff_start, diff_len in py_line_diffs[path]:
6215 diff_end = diff_start + diff_len - 1
6216 # Yapf errors out if diff_end < diff_start but this
6217 # is a valid line range diff for a removal.
6218 if diff_end >= diff_start:
6219 has_formattable_lines = True
6220 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
6221 # If all line diffs were removals we have nothing to format.
6222 if not has_formattable_lines:
6223 continue
6224
6225 if opts.diff or opts.dry_run:
6226 cmd += ['--diff']
6227 # Will return non-zero exit code if non-empty diff.
6228 stdout = RunCommand(cmd,
6229 error_ok=True,
6230 stderr=subprocess2.PIPE,
6231 cwd=top_dir,
6232 shell=sys.platform.startswith('win32'))
6233 if opts.diff:
6234 sys.stdout.write(stdout)
6235 elif len(stdout) > 0:
6236 return_value = 2
6237 else:
6238 cmd += ['-i']
6239 RunCommand(cmd, cwd=top_dir, shell=sys.platform.startswith('win32'))
6240 return return_value
6241
6242
6243def _RunGnFormat(opts, paths, top_dir, upstream_commit):
6244 cmd = ['gn', 'format']
6245 if opts.dry_run or opts.diff:
6246 cmd.append('--dry-run')
6247 return_value = 0
6248 for path in paths:
6249 gn_ret = subprocess2.call(cmd + [path],
6250 shell=sys.platform.startswith('win'),
6251 cwd=top_dir)
6252 if opts.dry_run and gn_ret == 2:
6253 return_value = 2 # Not formatted.
6254 elif opts.diff and gn_ret == 2:
6255 # TODO this should compute and print the actual diff.
6256 print('This change has GN build file diff for ' + path)
6257 elif gn_ret != 0:
6258 # For non-dry run cases (and non-2 return values for dry-run), a
6259 # nonzero error code indicates a failure, probably because the
6260 # file doesn't parse.
6261 DieWithError('gn format failed on ' + path +
6262 '\nTry running `gn format` on this file manually.')
6263 return return_value
6264
6265
6266def _FormatXml(opts, paths, top_dir, upstream_commit):
6267 # Skip the metrics formatting from the global presubmit hook. These files
6268 # have a separate presubmit hook that issues an error if the files need
6269 # formatting, whereas the top-level presubmit script merely issues a
6270 # warning. Formatting these files is somewhat slow, so it's important not to
6271 # duplicate the work.
6272 if opts.presubmit:
6273 return 0
6274
6275 return_value = 0
6276 for path in paths:
6277 xml_dir = GetMetricsDir(path)
6278 if not xml_dir:
6279 continue
6280
6281 tool_dir = os.path.join(top_dir, xml_dir)
6282 pretty_print_tool = os.path.join(tool_dir, 'pretty_print.py')
6283 cmd = [shutil.which('vpython3'), pretty_print_tool, '--non-interactive']
6284
6285 # If the XML file is histograms.xml or enums.xml, add the xml path
6286 # to the command as histograms/pretty_print.py now needs a relative
6287 # path argument after splitting the histograms into multiple
6288 # directories. For example, in tools/metrics/ukm, pretty-print could
6289 # be run using: $ python pretty_print.py But in
6290 # tools/metrics/histogrmas, pretty-print should be run with an
6291 # additional relative path argument, like: $ python pretty_print.py
6292 # metadata/UMA/histograms.xml $ python pretty_print.py enums.xml
6293 if xml_dir == os.path.join('tools', 'metrics', 'histograms'):
6294 if os.path.basename(path) not in ('histograms.xml', 'enums.xml',
6295 'histogram_suffixes_list.xml'):
6296 # Skip this XML file if it's not one of the known types.
6297 continue
6298 cmd.append(path)
6299
6300 if opts.dry_run or opts.diff:
6301 cmd.append('--diff')
6302
6303 stdout = RunCommand(cmd, cwd=top_dir)
6304 if opts.diff:
6305 sys.stdout.write(stdout)
6306 if opts.dry_run and stdout:
6307 return_value = 2 # Not formatted.
6308 return return_value
6309
6310
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006311def MatchingFileType(file_name, extensions):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006312 """Returns True if the file name ends with one of the given extensions."""
6313 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006314
Andrii Shyshkalov18975322017-01-25 16:44:13 +01006315
enne@chromium.org555cfe42014-01-29 18:21:39 +00006316@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006317@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006318def CMDformat(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006319 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Andrew Grievecca48db2023-09-14 14:12:23 +00006320 clang_exts = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006321 GN_EXTS = ['.gn', '.gni', '.typemap']
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006322 parser.add_option('--full',
6323 action='store_true',
6324 help='Reformat the full content of all touched files')
6325 parser.add_option('--upstream', help='Branch to check against')
6326 parser.add_option('--dry-run',
6327 action='store_true',
6328 help='Don\'t modify any file on disk.')
6329 parser.add_option(
6330 '--no-clang-format',
6331 dest='clang_format',
6332 action='store_false',
6333 default=True,
6334 help='Disables formatting of various file types using clang-format.')
6335 parser.add_option('--python',
6336 action='store_true',
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006337 help='Enables python formatting on all python files.')
6338 parser.add_option(
6339 '--no-python',
Andrew Grievecca48db2023-09-14 14:12:23 +00006340 action='store_false',
6341 dest='python',
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006342 help='Disables python formatting on all python files. '
6343 'If neither --python or --no-python are set, python files that have a '
6344 '.style.yapf file in an ancestor directory will be formatted. '
6345 'It is an error to set both.')
6346 parser.add_option('--js',
6347 action='store_true',
6348 help='Format javascript code with clang-format. '
6349 'Has no effect if --no-clang-format is set.')
6350 parser.add_option('--diff',
6351 action='store_true',
6352 help='Print diff to stdout rather than modifying files.')
6353 parser.add_option('--presubmit',
6354 action='store_true',
6355 help='Used when running the script from a presubmit.')
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006356
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006357 parser.add_option(
6358 '--rust-fmt',
6359 dest='use_rust_fmt',
6360 action='store_true',
6361 default=rustfmt.IsRustfmtSupported(),
6362 help='Enables formatting of Rust file types using rustfmt.')
6363 parser.add_option(
6364 '--no-rust-fmt',
6365 dest='use_rust_fmt',
6366 action='store_false',
6367 help='Disables formatting of Rust file types using rustfmt.')
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006368
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006369 parser.add_option(
6370 '--swift-format',
6371 dest='use_swift_format',
6372 action='store_true',
6373 default=swift_format.IsSwiftFormatSupported(),
6374 help='Enables formatting of Swift file types using swift-format '
6375 '(macOS host only).')
6376 parser.add_option(
6377 '--no-swift-format',
6378 dest='use_swift_format',
6379 action='store_false',
6380 help='Disables formatting of Swift file types using swift-format.')
Olivier Robin0a6b5442022-04-07 07:25:04 +00006381
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006382 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006383
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006384 # Normalize any remaining args against the current path, so paths relative
6385 # to the current directory are still resolved as expected.
6386 args = [os.path.join(os.getcwd(), arg) for arg in args]
Daniel Chengc55eecf2016-12-30 03:11:02 -08006387
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006388 # git diff generates paths against the root of the repository. Change
6389 # to that directory so clang-format can find files even within subdirs.
6390 rel_base_path = settings.GetRelativeRoot()
6391 if rel_base_path:
6392 os.chdir(rel_base_path)
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00006393
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006394 # Grab the merge-base commit, i.e. the upstream commit of the current
6395 # branch when it was created or the last time it was rebased. This is
6396 # to cover the case where the user may have called "git fetch origin",
6397 # moving the origin branch to a newer commit, but hasn't rebased yet.
6398 upstream_commit = None
6399 upstream_branch = opts.upstream
6400 if not upstream_branch:
6401 cl = Changelist()
6402 upstream_branch = cl.GetUpstreamBranch()
6403 if upstream_branch:
6404 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
6405 upstream_commit = upstream_commit.strip()
digit@chromium.org29e47272013-05-17 17:01:46 +00006406
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006407 if not upstream_commit:
6408 DieWithError('Could not find base commit for this branch. '
6409 'Are you in detached state?')
digit@chromium.org29e47272013-05-17 17:01:46 +00006410
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006411 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
6412 diff_output = RunGit(changed_files_cmd)
6413 diff_files = diff_output.splitlines()
6414 # Filter out files deleted by this CL
6415 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006416
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006417 if opts.js:
Andrew Grievecca48db2023-09-14 14:12:23 +00006418 clang_exts.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11006419
Andrew Grievecca48db2023-09-14 14:12:23 +00006420 formatters = [
6421 (GN_EXTS, _RunGnFormat),
6422 (['.xml'], _FormatXml),
Garrett Beatyed0cc5f2020-01-06 17:26:13 +00006423 ]
Andrew Grievecca48db2023-09-14 14:12:23 +00006424 if opts.clang_format:
6425 formatters += [(clang_exts, _RunClangFormatDiff)]
6426 if opts.use_rust_fmt:
6427 formatters += [(['.rs'], _RunRustFmt)]
6428 if opts.use_swift_format:
6429 formatters += [(['.swift'], _RunSwiftFormat)]
6430 if opts.python is not False:
6431 formatters += [(['.py'], _RunYapf)]
digit@chromium.org29e47272013-05-17 17:01:46 +00006432
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006433 top_dir = settings.GetRoot()
Andrew Grievecca48db2023-09-14 14:12:23 +00006434 return_value = 0
6435 for file_types, format_func in formatters:
6436 paths = [p for p in diff_files if MatchingFileType(p, file_types)]
6437 if not paths:
6438 continue
6439 ret = format_func(opts, paths, top_dir, upstream_commit)
6440 return_value = return_value or ret
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006441
6442 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006443
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00006444
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006445def GetMetricsDir(diff_xml):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006446 metrics_xml_dirs = [
6447 os.path.join('tools', 'metrics', 'actions'),
6448 os.path.join('tools', 'metrics', 'histograms'),
6449 os.path.join('tools', 'metrics', 'structured'),
6450 os.path.join('tools', 'metrics', 'ukm'),
6451 ]
6452 for xml_dir in metrics_xml_dirs:
6453 if diff_xml.startswith(xml_dir):
6454 return xml_dir
6455 return None
Steven Holte2e664bf2017-04-21 13:10:47 -07006456
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006457
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006458@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006459@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006460def CMDcheckout(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006461 """Checks out a branch associated with a given Gerrit issue."""
6462 _, args = parser.parse_args(args)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006463
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006464 if len(args) != 1:
6465 parser.print_help()
6466 return 1
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006467
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006468 issue_arg = ParseIssueNumberArgument(args[0])
6469 if not issue_arg.valid:
6470 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02006471
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006472 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006473
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006474 output = RunGit([
6475 'config', '--local', '--get-regexp', r'branch\..*\.' + ISSUE_CONFIG_KEY
6476 ],
6477 error_ok=True)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006478
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006479 branches = []
6480 for key, issue in [x.split() for x in output.splitlines()]:
6481 if issue == target_issue:
6482 branches.append(
6483 re.sub(r'branch\.(.*)\.' + ISSUE_CONFIG_KEY, r'\1', key))
Edward Lemur52969c92020-02-06 18:15:28 +00006484
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006485 if len(branches) == 0:
6486 print('No branch found for issue %s.' % target_issue)
6487 return 1
6488 if len(branches) == 1:
6489 RunGit(['checkout', branches[0]])
6490 else:
6491 print('Multiple branches match issue %s:' % target_issue)
6492 for i in range(len(branches)):
6493 print('%d: %s' % (i, branches[i]))
6494 which = gclient_utils.AskForData('Choose by index: ')
6495 try:
6496 RunGit(['checkout', branches[int(which)]])
6497 except (IndexError, ValueError):
6498 print('Invalid selection, not checking out any branch.')
6499 return 1
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006500
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006501 return 0
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006502
6503
maruel@chromium.org29404b52014-09-08 22:58:00 +00006504def CMDlol(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006505 # This command is intentionally undocumented.
6506 print(
6507 zlib.decompress(
6508 base64.b64decode(
6509 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
6510 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
6511 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
6512 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')).decode('utf-8'))
6513 return 0
maruel@chromium.org29404b52014-09-08 22:58:00 +00006514
6515
Josip Sokcevic0399e172022-03-21 23:11:51 +00006516def CMDversion(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006517 import utils
6518 print(utils.depot_tools_version())
Josip Sokcevic0399e172022-03-21 23:11:51 +00006519
6520
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006521class OptionParser(optparse.OptionParser):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006522 """Creates the option parse and add --verbose support."""
6523 def __init__(self, *args, **kwargs):
6524 optparse.OptionParser.__init__(self,
6525 *args,
6526 prog='git cl',
6527 version=__version__,
6528 **kwargs)
6529 self.add_option('-v',
6530 '--verbose',
6531 action='count',
6532 default=0,
6533 help='Use 2 times for more debugging info')
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00006534
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006535 def parse_args(self, args=None, _values=None):
Joanna Wangc5b38322023-03-15 20:38:46 +00006536 try:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006537 return self._parse_args(args)
6538 finally:
6539 # Regardless of success or failure of args parsing, we want to
6540 # report metrics, but only after logging has been initialized (if
6541 # parsing succeeded).
6542 global settings
6543 settings = Settings()
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00006544
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006545 if metrics.collector.config.should_collect_metrics:
6546 try:
6547 # GetViewVCUrl ultimately calls logging method.
6548 project_url = settings.GetViewVCUrl().strip('/+')
6549 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
6550 metrics.collector.add('project_urls', [project_url])
6551 except subprocess2.CalledProcessError:
6552 # Occurs when command is not executed in a git repository
6553 # We should not fail here. If the command needs to be
6554 # executed in a repo, it will be raised later.
6555 pass
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006556
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006557 def _parse_args(self, args=None):
6558 # Create an optparse.Values object that will store only the actual
6559 # passed options, without the defaults.
6560 actual_options = optparse.Values()
6561 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
6562 # Create an optparse.Values object with the default options.
6563 options = optparse.Values(self.get_default_values().__dict__)
6564 # Update it with the options passed by the user.
6565 options._update_careful(actual_options.__dict__)
6566 # Store the options passed by the user in an _actual_options attribute.
6567 # We store only the keys, and not the values, since the values can
6568 # contain arbitrary information, which might be PII.
6569 metrics.collector.add('arguments', list(actual_options.__dict__.keys()))
Edward Lemur83bd7f42018-10-10 00:14:21 +00006570
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006571 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
6572 logging.basicConfig(
6573 level=levels[min(options.verbose,
6574 len(levels) - 1)],
6575 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
6576 '%(filename)s] %(message)s')
6577
6578 return options, args
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006579
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006580
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006581def main(argv):
Gavin Mak7f5b53f2023-09-07 18:13:01 +00006582 if sys.version_info[0] < 3:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006583 print('\nYour Python version %s is unsupported, please upgrade.\n' %
6584 (sys.version.split(' ', 1)[0], ),
6585 file=sys.stderr)
6586 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006587
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006588 colorize_CMDstatus_doc()
6589 dispatcher = subcommand.CommandDispatcher(__name__)
6590 try:
6591 return dispatcher.execute(OptionParser(), argv)
6592 except auth.LoginRequiredError as e:
6593 DieWithError(str(e))
6594 except urllib.error.HTTPError as e:
6595 if e.code != 500:
6596 raise
6597 DieWithError((
6598 'App Engine is misbehaving and returned HTTP %d, again. Keep faith '
6599 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
6600 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006601
6602
6603if __name__ == '__main__':
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006604 # These affect sys.stdout, so do it outside of main() to simplify mocks in
6605 # the unit tests.
6606 fix_encoding.fix_encoding()
6607 setup_color.init()
6608 with metrics.collector.print_notice_and_exit():
6609 sys.exit(main(sys.argv[1:]))