Skip to content

Commit 950e55d

Browse files
authored
Merge branch 'main' into unifying_the_style_checkers
2 parents 6b553db + 69bd273 commit 950e55d

1 file changed

Lines changed: 340 additions & 0 deletions

File tree

sbin/tagman

Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
#!/usr/bin/env bash
2+
# -----------------------------------------------------------------------------
3+
# (C) Crown copyright Met Office. All rights reserved.
4+
# The file LICENCE, distributed with this code, contains details of the terms
5+
# under which the code may be used.
6+
# -----------------------------------------------------------------------------
7+
#
8+
# Script to manage Git tags (add/delete/list).
9+
# Requires:
10+
# GitHub CLI (gh): https://cli.github.com/
11+
# Git command-line tool: https://git-scm.com/
12+
# jq for JSON parsing: https://stedolan.github.io/jq/
13+
# Warnings:
14+
# - This script modifies Git tags. Use with caution.
15+
# - Always verify the current tags before making changes.
16+
# - Anyone with push/write access to the repository can create or delete tags.
17+
# Ensure you have the necessary permissions and understand the implications
18+
# of modifying tags in a shared repository.
19+
20+
set -euo pipefail
21+
# Colour codes for output
22+
GREEN='\033[0;32m'
23+
RED='\033[0;31m'
24+
YELLOW='\033[0;33m'
25+
NO_COLOR='\033[0m'
26+
27+
# Default values
28+
DEFAULT_REPO="MetOffice/git_playground"
29+
REPO="${REPO:-$DEFAULT_REPO}"
30+
# Variables set by parse_args()
31+
REF=""
32+
TAG=""
33+
MESSAGE=""
34+
DRY_RUN=false
35+
36+
usage() {
37+
cat <<EOF
38+
Usage:
39+
$(basename "$0") add <tag_name> <commit_ref> [options]
40+
$(basename "$0") delete|del <tag_name> [options]
41+
$(basename "$0") list|ls [options]
42+
43+
Actions:
44+
add Create and push a new tag
45+
delete Delete a tag from the repository (alias: del)
46+
list List all tags in the repository (alias: ls)
47+
48+
Arguments:
49+
<tag_name> Name of the tag to create or delete
50+
<commit_ref> Commit SHA, tag name, release name, or branch name
51+
52+
Options:
53+
--repo, -R REPO Repository in format owner/repo (default: $DEFAULT_REPO)
54+
--message MSG Tag annotation message (for add action)
55+
--dry-run, -n Show what would be done without making changes
56+
57+
Examples:
58+
# Create tag from commit SHA or existing tag or release or branch
59+
$(basename "$0") add Test abc123def|vn1.5|main --repo MetOffice/git_playground
60+
61+
# Delete tag
62+
$(basename "$0") del 2025.12.0 --repo MetOffice/SimSys_Scripts
63+
64+
Notes:
65+
- REPO can be set via environment variable (default: $DEFAULT_REPO)
66+
- All other parameters must be provided via command-line arguments
67+
- Use --dry-run to preview changes before executing
68+
EOF
69+
exit 1
70+
}
71+
72+
cleanup() {
73+
if [[ -n "${WORK_TMP:-}" ]]; then
74+
rm -rf "$WORK_TMP"
75+
fi
76+
}
77+
78+
confirm() {
79+
local message="$1"
80+
local response
81+
echo -en "${YELLOW}"
82+
read -rp "$message (y/n): " response
83+
echo -en "${NO_COLOR}"
84+
85+
case "${response^^}" in # Note: requires bash 4+ for ^^ operator
86+
YES | Y)
87+
return 0 ;;
88+
*)
89+
echo "Aborted..."
90+
return 1 ;;
91+
esac
92+
}
93+
94+
run() {
95+
local msg="$1"
96+
shift
97+
local timestamp
98+
timestamp=$(date "+%F %T")
99+
100+
if "$@"; then
101+
echo -e "[$timestamp] ${GREEN}${NO_COLOR} $msg succeeded."
102+
return 0
103+
else
104+
echo -e "[$timestamp] ${RED}${NO_COLOR} $msg failed."
105+
return 1
106+
fi
107+
}
108+
109+
trap cleanup EXIT ERR SIGINT
110+
111+
verify_tag() {
112+
if gh api "repos/${REPO}/git/refs/tags/${TAG}" >/dev/null 2>&1; then
113+
echo -e "${YELLOW}Tag '$TAG' exists in repository '$REPO'.${NO_COLOR}"
114+
return 0
115+
fi
116+
return 1
117+
}
118+
119+
verify_ref() {
120+
local resolved_sha=""
121+
122+
# First, try to resolve as a commit SHA (handles both short and full)
123+
if resolved_sha=$(gh api "repos/${REPO}/commits/${REF}" --jq '.sha' 2>/dev/null); then
124+
REF="$resolved_sha"
125+
echo -e "${GREEN}Using commit SHA: $REF${NO_COLOR}"
126+
return 0
127+
fi
128+
129+
# Try to resolve as a tag
130+
if gh api "repos/${REPO}/git/refs/tags/${REF}" >/dev/null 2>&1; then
131+
echo -e "${YELLOW}Resolving tag '$REF' to commit SHA...${NO_COLOR}"
132+
local tag_sha
133+
tag_sha=$(gh api "repos/${REPO}/git/refs/tags/${REF}" --jq '.object.sha')
134+
135+
# Try to get tag object to determine if it's annotated
136+
local tag_info
137+
if tag_info=$(gh api "repos/${REPO}/git/tags/${tag_sha}" 2>/dev/null); then
138+
# It's an annotated tag - get the commit SHA it points to
139+
local tag_type
140+
tag_type=$(echo "$tag_info" | jq -r '.object.type')
141+
142+
if [[ "$tag_type" == "commit" ]]; then
143+
resolved_sha=$(echo "$tag_info" | jq -r '.object.sha')
144+
else
145+
echo -e "${RED}** Tag points to object type: $tag_type, but a commit object type is expected${NO_COLOR}"
146+
return 1
147+
fi
148+
else
149+
# It's a lightweight tag - the SHA is the commit SHA
150+
resolved_sha="$tag_sha"
151+
fi
152+
153+
# Verify it's a full SHA and a valid commit
154+
if resolved_sha=$(gh api "repos/${REPO}/commits/${resolved_sha}" --jq '.sha' 2>/dev/null); then
155+
REF="$resolved_sha"
156+
echo -e "${GREEN}Resolved to commit: $REF${NO_COLOR}"
157+
return 0
158+
else
159+
echo -e "${RED}** Failed to verify commit SHA from tag${NO_COLOR}"
160+
return 1
161+
fi
162+
fi
163+
164+
# Try to resolve as a release
165+
if gh api "repos/${REPO}/releases/tags/${REF}" >/dev/null 2>&1; then
166+
echo -e "${YELLOW}Resolving release '$REF' to commit SHA...${NO_COLOR}"
167+
local target_ref
168+
target_ref=$(gh api "repos/${REPO}/releases/tags/${REF}" --jq '.target_commitish')
169+
170+
# Resolve the target to full SHA
171+
if resolved_sha=$(gh api "repos/${REPO}/commits/${target_ref}" --jq '.sha' 2>/dev/null); then
172+
REF="$resolved_sha"
173+
echo -e "${GREEN}Resolved to commit: $REF${NO_COLOR}"
174+
return 0
175+
else
176+
echo -e "${RED}** Failed to resolve release target to commit SHA${NO_COLOR}"
177+
return 1
178+
fi
179+
fi
180+
181+
# Try as a branch name
182+
if gh api "repos/${REPO}/git/refs/heads/${REF}" >/dev/null 2>&1; then
183+
echo -e "${YELLOW}Resolving branch '$REF' to commit SHA...${NO_COLOR}"
184+
local branch_sha
185+
branch_sha=$(gh api "repos/${REPO}/git/refs/heads/${REF}" --jq '.object.sha')
186+
187+
# Verify it's a full SHA
188+
if resolved_sha=$(gh api "repos/${REPO}/commits/${branch_sha}" --jq '.sha' 2>/dev/null); then
189+
REF="$resolved_sha"
190+
echo -e "${GREEN}Resolved to commit: $REF${NO_COLOR}"
191+
return 0
192+
else
193+
echo -e "${RED}** Failed to verify commit SHA from branch${NO_COLOR}"
194+
return 1
195+
fi
196+
fi
197+
198+
echo -e "${RED}** Reference '$REF' not found in repository '$REPO'.${NO_COLOR}"
199+
echo -e "${RED}** Tried: commit SHA, tag, release, and branch name.${NO_COLOR}"
200+
return 1
201+
}
202+
203+
add_tag() {
204+
if [[ -z "$TAG" || -z "$REF" ]]; then
205+
echo -e "${RED}** TAG and REF are required for add action.${NO_COLOR}"
206+
usage
207+
fi
208+
209+
if verify_tag ; then
210+
exit 1 # Tag already exists, exit with error
211+
fi
212+
if ! verify_ref ; then
213+
exit 1 # Reference resolution failed, exit with error
214+
fi
215+
216+
local url="https://github.com/${REPO}.git"
217+
local msg="${MESSAGE:-"Tagging $TAG @ $REF"}"
218+
219+
if [[ "$DRY_RUN" == true ]]; then
220+
echo -e "${YELLOW}[DRY RUN] Would create tag with the following details:${NO_COLOR}"
221+
echo -e " Repository: $REPO"
222+
echo -e " Tag name: $TAG"
223+
echo -e " Commit SHA: $REF"
224+
echo -e " Message: $msg"
225+
echo -e "${YELLOW}[DRY RUN] No changes made.${NO_COLOR}"
226+
return 0
227+
fi
228+
229+
WORK_TMP=$(mktemp -d -t tagman-XXXX)
230+
231+
pushd "$WORK_TMP" >/dev/null
232+
run "Initialise temporary Git repository in $WORK_TMP" git init --bare --quiet
233+
run "Add remote repository $url" git remote add origin "$url"
234+
run "Fetch commit $REF" git fetch --quiet --depth 1 origin "$REF"
235+
run "Create and sign tag '$TAG' at commit $REF" \
236+
git tag --sign "$TAG" "$REF" --message "$msg"
237+
run "Push '$TAG' to remote '$REPO'" git push --quiet origin "$TAG"
238+
popd >/dev/null
239+
240+
echo -e "${GREEN}Successfully created and pushed tag '$TAG' to '$REPO'.${NO_COLOR}"
241+
}
242+
243+
delete_tag() {
244+
if [[ -z "$TAG" ]]; then
245+
echo -e "${RED}** TAG is required for delete action.${NO_COLOR}"
246+
usage
247+
fi
248+
249+
if ! verify_tag; then
250+
echo -e "${RED}** Tag '$TAG' does not exist in repository '$REPO'.${NO_COLOR}"
251+
return 1
252+
fi
253+
254+
if [[ "$DRY_RUN" == true ]]; then
255+
echo -e "${YELLOW}[DRY RUN] Would delete tag with the following details:${NO_COLOR}"
256+
echo -e " Repository: $REPO"
257+
echo -e " Tag name: $TAG"
258+
echo -e "${YELLOW}[DRY RUN] No changes made.${NO_COLOR}"
259+
return 0
260+
fi
261+
262+
local url="https://github.com/${REPO}.git"
263+
264+
WORK_TMP=$(mktemp -d -t tagman-XXXX)
265+
266+
pushd "$WORK_TMP" >/dev/null
267+
run "Initialise temporary Git repository in $WORK_TMP" git init --bare --quiet
268+
run "Add remote repository $url" git remote add origin "$url"
269+
270+
if confirm "Are you sure you want to delete the tag '$TAG' from '$REPO'?"; then
271+
run "Delete remote tag '$TAG' from '$REPO'" git push --quiet origin --delete "$TAG"
272+
echo -e "${GREEN}Successfully deleted tag '$TAG' from '$REPO'.${NO_COLOR}"
273+
fi
274+
popd >/dev/null
275+
}
276+
277+
list_tags() {
278+
local url="https://github.com/${REPO}.git"
279+
echo -e "${GREEN}Listing tags from '$REPO':${NO_COLOR}\n"
280+
git ls-remote --tags --sort="-version:refname" "$url"
281+
}
282+
283+
parse_args() {
284+
if [[ $# -eq 0 ]]; then
285+
usage
286+
fi
287+
288+
ACTION="$1"
289+
shift
290+
291+
case "$ACTION" in
292+
add)
293+
if [[ $# -lt 2 ]]; then
294+
usage
295+
fi
296+
TAG="$1"
297+
REF="$2"
298+
shift 2
299+
;;
300+
del|delete)
301+
if [[ $# -lt 1 ]]; then
302+
usage
303+
fi
304+
TAG="$1"
305+
shift
306+
;;
307+
ls|list)
308+
# No arguments required
309+
;;
310+
*)
311+
echo -e "${RED}Unknown action: $ACTION${NO_COLOR}"
312+
usage
313+
;;
314+
esac
315+
316+
# Parse optional flags
317+
while [[ $# -gt 0 ]]; do
318+
case "$1" in
319+
-R|--repo) REPO="$2"; shift 2 ;;
320+
--message) MESSAGE="$2"; shift 2 ;;
321+
-n|--dry-run) DRY_RUN=true; shift ;;
322+
*)
323+
echo -e "${RED}Unknown option: $1${NO_COLOR}"
324+
usage ;;
325+
esac
326+
done
327+
}
328+
329+
main() {
330+
parse_args "$@"
331+
332+
case "$ACTION" in
333+
add) add_tag ;;
334+
del|delete) delete_tag ;;
335+
ls|list) list_tags ;;
336+
*) echo -e "${RED}Invalid action: $ACTION${NO_COLOR}" ;;
337+
esac
338+
}
339+
340+
main "$@"

0 commit comments

Comments
 (0)