148 lines
5.1 KiB
Python
148 lines
5.1 KiB
Python
import os
|
|
import argparse
|
|
import sys
|
|
import re
|
|
import subprocess
|
|
from typing import List, Dict
|
|
from pathlib import Path
|
|
|
|
SCRIPT_DIR = os.path.dirname(__file__)
|
|
|
|
parser = argparse.ArgumentParser(description="Generate a patch file for a DuckDB extension.")
|
|
|
|
parser.add_argument(
|
|
"repository_path",
|
|
type=str,
|
|
help="Path to the repository where the changes live that should be turned into a patch.",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"extension_name",
|
|
type=str,
|
|
help="Name of the extension to patch, should match the name in `.github/config/extensions/<extension_name>.cmake`.",
|
|
)
|
|
|
|
parser.add_argument("patch_name", type=str, help="Name for the patch file to create.")
|
|
|
|
parser.add_argument("--overwrite", action="store_true", help="Allow overwriting the patch file if it already exists.")
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
def verify_git_tag():
|
|
# Locate the cmake file to extract the GIT_TAG from
|
|
cmake_path = Path(SCRIPT_DIR) / '..' / ".github" / "config" / "extensions" / f"{args.extension_name}.cmake"
|
|
if not cmake_path.is_file():
|
|
print(f"Error: Extension CMake file not found: {cmake_path}")
|
|
sys.exit(1)
|
|
|
|
cmake_content = cmake_path.read_text()
|
|
|
|
# Extract GIT_TAG from the cmake file
|
|
match = re.search(r"\bGIT_TAG\s+([^\s\)]+)", cmake_content)
|
|
if not match:
|
|
print(f"Error: Could not find GIT_TAG in {cmake_path}")
|
|
sys.exit(1)
|
|
|
|
git_tag_in_cmake = match.group(1)
|
|
|
|
# Get the current commit hash in repository_path
|
|
try:
|
|
result = subprocess.run(
|
|
["git", "rev-parse", "HEAD"],
|
|
cwd=args.repository_path,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True,
|
|
check=True,
|
|
)
|
|
current_commit = result.stdout.strip()
|
|
except subprocess.CalledProcessError as e:
|
|
print(f"Error: Failed to run git in {args.repository_path} — {e.stderr.strip()}")
|
|
sys.exit(1)
|
|
|
|
# Compare the tags
|
|
if git_tag_in_cmake != current_commit:
|
|
print(
|
|
f"Error: GIT_TAG in {cmake_path} is {git_tag_in_cmake}, "
|
|
f"but repository {args.repository_path} is checked out at {current_commit}."
|
|
)
|
|
sys.exit(1)
|
|
|
|
|
|
def create_patch():
|
|
# Collect changes with git diff
|
|
try:
|
|
diff_result = subprocess.run(
|
|
["git", "diff", "--ignore-submodules"],
|
|
cwd=args.repository_path,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True,
|
|
check=True,
|
|
)
|
|
except subprocess.CalledProcessError as e:
|
|
print(f"Error: Failed to run git diff — {e.stderr.strip()}")
|
|
sys.exit(1)
|
|
|
|
new_patch_content = diff_result.stdout
|
|
if not new_patch_content.strip():
|
|
print("⚠️ No changes detected in repository; no patch will be created.")
|
|
sys.exit(0)
|
|
|
|
def parse_patch_files_and_lines(patch_text):
|
|
changes = {}
|
|
current_file = None
|
|
for line in patch_text.splitlines():
|
|
if line.startswith("diff --git"):
|
|
parts = line.split()
|
|
if len(parts) >= 3:
|
|
# Format: diff --git a/file b/file
|
|
current_file = parts[2][2:] # remove 'a/'
|
|
changes.setdefault(current_file, set())
|
|
elif line.startswith("@@") and current_file:
|
|
# Format: @@ -old_start,old_count +new_start,new_count @@
|
|
m = re.match(r"@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@", line)
|
|
if m:
|
|
start = int(m.group(1))
|
|
length = int(m.group(2) or "1")
|
|
for l in range(start, start + length):
|
|
changes[current_file].add(l)
|
|
return changes
|
|
|
|
new_changes = parse_patch_files_and_lines(new_patch_content)
|
|
|
|
# Check conflicts with existing patches
|
|
patch_dir = (Path(SCRIPT_DIR) / ".." / ".github" / "patches" / "extensions" / args.extension_name).resolve()
|
|
patch_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
for existing_patch in patch_dir.glob("*.patch"):
|
|
if existing_patch.name == f"{args.patch_name}.patch":
|
|
if not args.overwrite:
|
|
print(f"A patch by the name '{args.patch_name}.patch' already exists, failed to create patch")
|
|
sys.exit(1)
|
|
else:
|
|
continue
|
|
existing_changes = parse_patch_files_and_lines(existing_patch.read_text())
|
|
|
|
for file, lines in new_changes.items():
|
|
if file in existing_changes:
|
|
overlap = lines & existing_changes[file]
|
|
if overlap:
|
|
print(f"❌ Conflict detected with existing patch: {existing_patch.name}")
|
|
print(f" File: {file}")
|
|
print(f" Overlapping lines: {sorted(overlap)}")
|
|
sys.exit(1)
|
|
|
|
# Save patch file
|
|
patch_dir = (Path(SCRIPT_DIR) / ".." / ".github" / "patches" / "extensions" / args.extension_name).resolve()
|
|
patch_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
patch_path = patch_dir / f"{args.patch_name}.patch"
|
|
patch_path.write_text(diff_result.stdout)
|
|
|
|
|
|
verify_git_tag()
|
|
|
|
create_patch()
|