mirror of
https://github.com/django/django.git
synced 2026-02-09 02:49:25 +08:00
This also fixed a small bash issue in `confirm_release.sh` script.
Backport of 532c1058a7 from main.
152 lines
4.6 KiB
Python
152 lines
4.6 KiB
Python
#! /usr/bin/env python3
|
|
import argparse
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
|
|
|
|
def run(cmd, *, cwd=None, env=None, dry_run=True):
|
|
"""Run a command with optional dry-run behavior."""
|
|
environ = os.environ.copy()
|
|
if env:
|
|
environ.update(env)
|
|
if dry_run:
|
|
print("[DRY RUN]", " ".join(cmd))
|
|
else:
|
|
print("[EXECUTE]", " ".join(cmd))
|
|
try:
|
|
result = subprocess.check_output(
|
|
cmd, cwd=cwd, env=environ, stderr=subprocess.STDOUT
|
|
)
|
|
except subprocess.CalledProcessError as e:
|
|
result = e.output
|
|
print(" [ERROR]", result)
|
|
raise
|
|
else:
|
|
print(" [RESULT]", result)
|
|
return result.decode().strip()
|
|
|
|
|
|
def validate_env(checkout_dir):
|
|
if not checkout_dir:
|
|
sys.exit("Error: checkout directory not provided (--checkout-dir).")
|
|
if not os.path.exists(checkout_dir):
|
|
sys.exit(f"Error: checkout directory '{checkout_dir}' does not exist.")
|
|
if not os.path.isdir(checkout_dir):
|
|
sys.exit(f"Error: '{checkout_dir}' is not a directory.")
|
|
|
|
|
|
def get_remote_branches(checkout_dir, include_fn):
|
|
"""Return list of remote branches filtered by include_fn."""
|
|
result = run(
|
|
["git", "branch", "--list", "-r"],
|
|
cwd=checkout_dir,
|
|
dry_run=False,
|
|
)
|
|
branches = [b.strip() for b in result.split("\n") if b.strip()]
|
|
return [b for b in branches if include_fn(b)]
|
|
|
|
|
|
def get_branch_info(checkout_dir, branch):
|
|
"""Return (commit_hash, last_update_date) for a given branch."""
|
|
commit_hash = run(["git", "rev-parse", branch], cwd=checkout_dir, dry_run=False)
|
|
last_update = run(
|
|
["git", "show", branch, "--format=format:%ai", "-s"],
|
|
cwd=checkout_dir,
|
|
dry_run=False,
|
|
)
|
|
return commit_hash, last_update
|
|
|
|
|
|
def create_tag(checkout_dir, branch, commit_hash, last_update, *, dry_run=True):
|
|
"""Create a tag locally for a given branch at its last update."""
|
|
tag_name = branch.replace("origin/", "", 1)
|
|
msg = f'"Tagged {tag_name} for EOL stable branch removal."'
|
|
run(
|
|
["git", "tag", "--sign", "--message", msg, tag_name, commit_hash],
|
|
cwd=checkout_dir,
|
|
env={"GIT_COMMITTER_DATE": last_update},
|
|
dry_run=dry_run,
|
|
)
|
|
return tag_name
|
|
|
|
|
|
def delete_remote_and_local_branch(checkout_dir, branch, *, dry_run=True):
|
|
"""Delete a remote branch from origin and the maching local branch."""
|
|
try:
|
|
run(
|
|
["git", "branch", "-D", branch],
|
|
cwd=checkout_dir,
|
|
dry_run=dry_run,
|
|
)
|
|
except subprocess.CalledProcessError:
|
|
print(f"[ERROR] Local branch {branch} can not be deleted.")
|
|
|
|
run(
|
|
["git", "push", "origin", "--delete", branch.replace("origin/", "", 1)],
|
|
cwd=checkout_dir,
|
|
dry_run=dry_run,
|
|
)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Archive Django branches into tags and optionally delete them."
|
|
)
|
|
parser.add_argument(
|
|
"--checkout-dir", required=True, help="Path to Django git checkout"
|
|
)
|
|
parser.add_argument(
|
|
"--dry-run",
|
|
action="store_true",
|
|
help="Print commands instead of executing them",
|
|
)
|
|
parser.add_argument(
|
|
"--branches", nargs="*", help="Specific remote branches to include (optional)"
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
validate_env(args.checkout_dir)
|
|
dry_run = args.dry_run
|
|
checkout_dir = args.checkout_dir
|
|
|
|
if args.branches:
|
|
wanted = set(f"origin/{b}" for b in args.branches)
|
|
else:
|
|
wanted = set()
|
|
|
|
branches = get_remote_branches(checkout_dir, include_fn=lambda b: b in wanted)
|
|
if not branches:
|
|
print("No branches matched inclusion criteria.")
|
|
return
|
|
|
|
print("\nMatched branches:")
|
|
print("\n".join(branches))
|
|
print()
|
|
|
|
branch_updates = {b: get_branch_info(checkout_dir, b) for b in branches}
|
|
print("\nLast updates:")
|
|
for b, (h, d) in branch_updates.items():
|
|
print(f"{b}\t{h}\t{d}")
|
|
|
|
if (
|
|
input("\nDelete remote branches and create tags? [y/N]: ").strip().lower()
|
|
== "y"
|
|
):
|
|
for b, (commit_hash, last_update_date) in branch_updates.items():
|
|
print(f"Creating tag for {b} at {commit_hash=} with {last_update_date=}")
|
|
create_tag(checkout_dir, b, commit_hash, last_update_date, dry_run=dry_run)
|
|
print(f"Deleting remote branch {b}")
|
|
delete_remote_and_local_branch(checkout_dir, b, dry_run=dry_run)
|
|
run(
|
|
["git", "push", "--tags"],
|
|
cwd=checkout_dir,
|
|
dry_run=dry_run,
|
|
)
|
|
|
|
print("Done.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|