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()
|