cb424d7448
verify-patch-sanity.py validates every active recipe .patch has internally- consistent hunk line counts — catching the 'malformed patch at line N' failure at commit/CI/preflight time instead of hours into a cook. This cycle hit that class three times (qtwaylandscanner, sddm, xwayland), each only discovered when cookbook tried to apply the patch. Running it across the repo found 29 latent malformed patches (validated against GNU patch: e.g. relibc/P3-sysv-ipc reproduces 'malformed patch at line 22'). They were harmless only because they sit in vendored recipes (baked, not re- applied) — but would fail on any version-bump re-derivation. --fix recounts the hunk headers (body untouched) and repaired all 29. Wired into build-preflight.sh (Phase 1.0D) and redbear-ci.yml, with a unit test (test-patch-sanity.sh). Skips archived/legacy trees and unvalidatable formats (empty placeholders, bare-@@ git hunks).
234 lines
6.7 KiB
Python
Executable File
234 lines
6.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
#
|
|
# ======- pre-push - LLVM Git Help Integration ---------*- python -*--========#
|
|
#
|
|
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
|
# See https://llvm.org/LICENSE.txt for license information.
|
|
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
#
|
|
# ==------------------------------------------------------------------------==#
|
|
|
|
"""
|
|
pre-push git hook integration
|
|
=============================
|
|
|
|
This script is intended to be setup as a pre-push hook, from the root of the
|
|
repo run:
|
|
|
|
ln -sf ../../llvm/utils/git/pre-push.py .git/hooks/pre-push
|
|
|
|
From the git doc:
|
|
|
|
The pre-push hook runs during git push, after the remote refs have been
|
|
updated but before any objects have been transferred. It receives the name
|
|
and location of the remote as parameters, and a list of to-be-updated refs
|
|
through stdin. You can use it to validate a set of ref updates before a push
|
|
occurs (a non-zero exit code will abort the push).
|
|
"""
|
|
|
|
import argparse
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from shlex import quote
|
|
|
|
VERBOSE = False
|
|
QUIET = False
|
|
dev_null_fd = None
|
|
z40 = "0000000000000000000000000000000000000000"
|
|
|
|
|
|
def eprint(*args, **kwargs):
|
|
print(*args, file=sys.stderr, **kwargs)
|
|
|
|
|
|
def log(*args, **kwargs):
|
|
if QUIET:
|
|
return
|
|
print(*args, **kwargs)
|
|
|
|
|
|
def log_verbose(*args, **kwargs):
|
|
if not VERBOSE:
|
|
return
|
|
print(*args, **kwargs)
|
|
|
|
|
|
def die(msg):
|
|
eprint(msg)
|
|
sys.exit(1)
|
|
|
|
|
|
def ask_confirm(prompt):
|
|
while True:
|
|
query = input("%s (y/N): " % (prompt))
|
|
if query.lower() not in ["y", "n", ""]:
|
|
print("Expect y or n!")
|
|
continue
|
|
return query.lower() == "y"
|
|
|
|
|
|
def shell(
|
|
cmd,
|
|
strip=True,
|
|
cwd=None,
|
|
stdin=None,
|
|
die_on_failure=True,
|
|
ignore_errors=False,
|
|
text=True,
|
|
print_raw_stderr=False,
|
|
):
|
|
# Escape args when logging for easy repro.
|
|
quoted_cmd = [quote(arg) for arg in cmd]
|
|
cwd_msg = ""
|
|
if cwd:
|
|
cwd_msg = " in %s" % cwd
|
|
log_verbose("Running%s: %s" % (cwd_msg, " ".join(quoted_cmd)))
|
|
|
|
# Silence errors if requested.
|
|
err_pipe = subprocess.DEVNULL if ignore_errors else subprocess.PIPE
|
|
|
|
start = time.time()
|
|
p = subprocess.Popen(
|
|
cmd,
|
|
cwd=cwd,
|
|
stdout=subprocess.PIPE,
|
|
stderr=err_pipe,
|
|
stdin=subprocess.PIPE,
|
|
universal_newlines=text,
|
|
)
|
|
stdout, stderr = p.communicate(input=stdin)
|
|
elapsed = time.time() - start
|
|
|
|
log_verbose("Command took %0.1fs" % elapsed)
|
|
|
|
if p.returncode == 0 or ignore_errors:
|
|
if stderr and not ignore_errors:
|
|
if not print_raw_stderr:
|
|
eprint("`%s` printed to stderr:" % " ".join(quoted_cmd))
|
|
eprint(stderr.rstrip())
|
|
if strip:
|
|
if text:
|
|
stdout = stdout.rstrip("\r\n")
|
|
else:
|
|
stdout = stdout.rstrip(b"\r\n")
|
|
if VERBOSE:
|
|
for l in stdout.splitlines():
|
|
log_verbose("STDOUT: %s" % l)
|
|
return stdout
|
|
err_msg = "`%s` returned %s" % (" ".join(quoted_cmd), p.returncode)
|
|
eprint(err_msg)
|
|
if stderr:
|
|
eprint(stderr.rstrip())
|
|
if die_on_failure:
|
|
sys.exit(2)
|
|
raise RuntimeError(err_msg)
|
|
|
|
|
|
def git(*cmd, **kwargs):
|
|
return shell(["git"] + list(cmd), **kwargs)
|
|
|
|
|
|
def get_revs_to_push(range):
|
|
commits = git("rev-list", range).splitlines()
|
|
# Reverse the order so we print the oldest commit first
|
|
commits.reverse()
|
|
return commits
|
|
|
|
|
|
def handle_push(args, local_ref, local_sha, remote_ref, remote_sha):
|
|
"""Check a single push request (which can include multiple revisions)"""
|
|
log_verbose(
|
|
"Handle push, reproduce with "
|
|
"`echo %s %s %s %s | pre-push.py %s %s"
|
|
% (local_ref, local_sha, remote_ref, remote_sha, args.remote, args.url)
|
|
)
|
|
# Handle request to delete
|
|
if local_sha == z40:
|
|
if not ask_confirm(
|
|
'Are you sure you want to delete "%s" on remote "%s"?'
|
|
% (remote_ref, args.url)
|
|
):
|
|
die("Aborting")
|
|
return
|
|
|
|
# Push a new branch
|
|
if remote_sha == z40:
|
|
if not ask_confirm(
|
|
'Are you sure you want to push a new branch/tag "%s" on remote "%s"?'
|
|
% (remote_ref, args.url)
|
|
):
|
|
die("Aborting")
|
|
range = local_sha
|
|
return
|
|
else:
|
|
# Update to existing branch, examine new commits
|
|
range = "%s..%s" % (remote_sha, local_sha)
|
|
# Check that the remote commit exists, otherwise let git proceed
|
|
if "commit" not in git("cat-file", "-t", remote_sha, ignore_errors=True):
|
|
return
|
|
|
|
revs = get_revs_to_push(range)
|
|
if not revs:
|
|
# This can happen if someone is force pushing an older revision to a branch
|
|
return
|
|
|
|
# Print the revision about to be pushed commits
|
|
print('Pushing to "%s" on remote "%s"' % (remote_ref, args.url))
|
|
for sha in revs:
|
|
print(" - " + git("show", "--oneline", "--quiet", sha))
|
|
|
|
if len(revs) > 1:
|
|
if not ask_confirm("Are you sure you want to push %d commits?" % len(revs)):
|
|
die("Aborting")
|
|
|
|
for sha in revs:
|
|
msg = git("log", "--format=%B", "-n1", sha)
|
|
if "Differential Revision" not in msg:
|
|
continue
|
|
for line in msg.splitlines():
|
|
for tag in ["Summary", "Reviewers", "Subscribers", "Tags"]:
|
|
if line.startswith(tag + ":"):
|
|
eprint(
|
|
'Please remove arcanist tags from the commit message (found "%s" tag in %s)'
|
|
% (tag, sha[:12])
|
|
)
|
|
if len(revs) == 1:
|
|
eprint("Try running: llvm/utils/git/arcfilter.sh")
|
|
die('Aborting (force push by adding "--no-verify")')
|
|
|
|
return
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if not shutil.which("git"):
|
|
die("error: cannot find git command")
|
|
|
|
argv = sys.argv[1:]
|
|
p = argparse.ArgumentParser(
|
|
prog="pre-push",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
description=__doc__,
|
|
)
|
|
verbosity_group = p.add_mutually_exclusive_group()
|
|
verbosity_group.add_argument(
|
|
"-q", "--quiet", action="store_true", help="print less information"
|
|
)
|
|
verbosity_group.add_argument(
|
|
"-v", "--verbose", action="store_true", help="print more information"
|
|
)
|
|
|
|
p.add_argument("remote", type=str, help="Name of the remote")
|
|
p.add_argument("url", type=str, help="URL for the remote")
|
|
|
|
args = p.parse_args(argv)
|
|
VERBOSE = args.verbose
|
|
QUIET = args.quiet
|
|
|
|
lines = sys.stdin.readlines()
|
|
sys.stdin = open("/dev/tty", "r")
|
|
for line in lines:
|
|
local_ref, local_sha, remote_ref, remote_sha = line.split()
|
|
handle_push(args, local_ref, local_sha, remote_ref, remote_sha)
|