summaryrefslogtreecommitdiff
path: root/scripts/archive_eol_stable_branches.py
blob: c2cafc9c5c43b7b1d0ea0db07bcf4b57e7153729 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
#! /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()