-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathgit-note-create.sh
More file actions
executable file
·191 lines (172 loc) · 5.08 KB
/
git-note-create.sh
File metadata and controls
executable file
·191 lines (172 loc) · 5.08 KB
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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
#!/usr/bin/env bash
#
# git-note-create.sh
#
# Quick git-notes writer with a "heading/body" CLI similar to `git ta`.
#
# Usage:
# git gn [^<notes-ref>] <heading...> [-- <body...>] [@<commit-ish>]
# git gn --ref <notes-ref> <heading...> [-- <body...>] [--at <commit-ish>]
#
# Shorthands:
# - `@<commit-ish>` sets the target object (default: HEAD)
# - `@-<N>` is sugar for `@HEAD~<N>` (e.g. `@-1` -> previous commit)
# - `^<notes-ref>` or `:<notes-ref>` sets the notes ref (default: refs/notes/commits)
# If <notes-ref> does not start with `refs/notes/`, it is treated as a
# short name and expanded to `refs/notes/<notes-ref>`.
#
# Behavior:
# - Writes an entry to git notes using `append` (so multiple notes accumulate).
# - The "heading" becomes the first line; optional body becomes subsequent lines.
# - Notes are stored on a separate ref (for example `refs/notes/commits`), not
# on the commit itself and not on your branch.
#
# Workflow:
# - You can add a note before or after pushing the target commit; the commit
# itself is unchanged either way.
# - Notes do not travel with normal branch `git push` / `git pull`. After
# changing notes, publish them separately with `git gnp [remote]`.
# - Before adding notes in another clone, or after time away, run
# `git gnl [remote]` first. That fetch is ff-only for `refs/notes/*`, so it
# rejects on divergence instead of overwriting your local notes.
# - If `git gnl` or `git gnp` rejects, run `git gnm [remote]` to reconcile
# remote notes into your local notes refs, then push again with
# `git gnp [remote]`.
# - Force-push notes only when you intentionally want local notes history to
# replace the remote notes history.
#
# Options:
# -e, --edit Open editor to edit note instead of using args.
# -R, --replace Replace existing note (vs append).
# --ref <notes-ref> Notes ref to write to.
# --at <commit-ish> Commit/object to attach note to.
# -h, --help Show help.
set -euo pipefail
usage() {
sed -n '2,/^set -euo pipefail$/p' "$0" | sed '$d' | sed 's/^# \{0,1\}//'
}
git rev-parse --git-dir >/dev/null 2>&1 || { echo "error: not in a git repo" >&2; exit 2; }
notes_ref="refs/notes/commits"
commitish="HEAD"
commitish_set=false
edit=false
replace=false
positional=()
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
usage
exit 0
;;
--)
shift
positional+=(-- "$@")
break
;;
-e|--edit)
edit=true
shift
;;
-R|--replace)
replace=true
shift
;;
--ref)
notes_ref="${2:-}"
[[ -n "$notes_ref" ]] || { echo "error: --ref requires a value" >&2; exit 2; }
shift 2
;;
--at)
commitish="${2:-}"
[[ -n "$commitish" ]] || { echo "error: --at requires a value" >&2; exit 2; }
commitish_set=true
shift 2
;;
-*)
echo "error: unknown option: $1" >&2
usage >&2
exit 2
;;
*)
positional+=("$1")
shift
;;
esac
done
expand_notes_ref() {
local input="$1"
if [[ "$input" == refs/notes/* ]]; then
printf '%s' "$input"
else
printf 'refs/notes/%s' "$input"
fi
}
heading_parts=()
body_parts=()
in_body=false
for arg in "${positional[@]}"; do
if [[ "$arg" == -- ]]; then
in_body=true
continue
fi
# After `--`, treat everything literally as note body (no @/^ shorthands).
if [[ "$in_body" == true ]]; then
body_parts+=("$arg")
continue
fi
if [[ "$arg" == @* ]]; then
commitish="${arg#@}"
if [[ "$commitish" =~ ^-([0-9]+)(.*)$ ]]; then
commitish="HEAD~${BASH_REMATCH[1]}${BASH_REMATCH[2]}"
fi
commitish_set=true
continue
fi
if [[ "$arg" =~ ^:[A-Za-z].* ]]; then
notes_ref="$(expand_notes_ref "${arg#:}")"
continue
fi
if [[ "$arg" == ^* ]]; then
notes_ref="$(expand_notes_ref "${arg#^}")"
continue
fi
# Convenience: in edit mode, allow a bare commit-ish (no leading `@`) so
# `gne <sha>` edits the note on that object.
if [[ "$edit" == true && "$commitish_set" == false ]]; then
commitish="$arg"
commitish_set=true
continue
fi
if [[ "$in_body" == true ]]; then
body_parts+=("$arg")
else
heading_parts+=("$arg")
fi
done
target_sha="$(git rev-parse --verify "${commitish}^{object}" 2>/dev/null || true)"
if [[ -z "$target_sha" ]]; then
echo "error: invalid object/commit-ish: $commitish" >&2
exit 2
fi
if [[ "$edit" == true ]]; then
git notes --ref "$notes_ref" edit "$target_sha"
printf 'gn: edited note (%s) on %s\n' "${notes_ref#refs/notes/}" "${target_sha:0:12}"
exit 0
fi
if [[ ${#heading_parts[@]} -eq 0 ]]; then
echo "error: missing heading (first line of note)" >&2
usage >&2
exit 2
fi
heading="${heading_parts[*]}"
body="${body_parts[*]}"
message="$heading"
if [[ -n "$body" ]]; then
message+=$'\n\n'"$body"
fi
if [[ "$replace" == true ]]; then
git notes --ref "$notes_ref" add -f -m "$message" "$target_sha"
else
git notes --ref "$notes_ref" append -m "$message" "$target_sha"
fi
printf 'gn: wrote note (%s) on %s\n' "${notes_ref#refs/notes/}" "${target_sha:0:12}"