]> Lady’s Gitweb - HOMEDIR/blob - sh/githooks/pre-commit.sh
Initial commit
[HOMEDIR] / sh / githooks / pre-commit.sh
1 #!/usr/bin/env sh
2 # @(#)🏠📂 HOMEDIR sh/githooks/pre-commit.sh 2026-03-22T02:03:39Z
3 # SPDX-FileCopyrightText: 2026 Lady <https://www.ladys.computer/about/#lady>
4 # SPDX-License-Identifier: MPL-2.0
5
6 ## ⁌ Git Pre‐Commit
7 ##
8 ## ∎ Copyright © 2026 Lady [@ Ladys Computer].
9 ##
10 ## ⋮ This Source Code Form is subject to the terms of the Mozilla
11 ## Public License, version 2.0.
12 ## If a copy of the M·P·L was not distributed with this file, You can
13 ## obtain one at {🔗<https://mozilla.org/MPL/2.0/>}.
14
15 ## § Description
16 ##
17 ## This script performs actions on files prior to a Git commit being
18 ## made.
19 ## It is intended to be used as a precommit hook in a Git repository.
20 ##
21 ## ❦ “What” signature updates
22 ##
23 ## This script checks the first 24 lines of each file that is currently
24 ## staged in the index (i·e which will be committed) for the string
25 ## `@({U+23})´.
26 ## It takes this, and all following characters up to the next newline
27 ## or tab (which are assumed to be a product name), and deletes
28 ## everything which follows on that line.
29 ## Then it appends to the line the following items, each preceded by a
30 ## tab :⁠—
31 ##
32 ## • The path to the file, relative to the repository root.
33 ##
34 ## • The current date and time.
35 ##
36 ## This operation is applied both on the version of the file staged in
37 ## the index and on the one in the working tree.
38 ##
39 ## The result of this change is that calling What (a Posix development
40 ## utility) on the file will provide the associated project name,
41 ## path, and datetime (of last commit) for the file.
42 ## This is useful in situations where more sophisticated versioning
43 ## identification (e·g `git describe´) is not available.
44 ##
45 ## If a file does not contain the sequence `@({U+23})´ in its first 24
46 ## lines, no changes are made.
47
48 ## § Configuration
49 ##
50 ## The following variables provide hooks to override the commands used
51 ## by this script.
52
53 : "${cmd_DATE:=date}"
54 : "${cmd_GIT:=git}"
55 : "${cmd_GREP:=grep}"
56 : "${cmd_HEAD:=head}"
57 : "${cmd_PRINTF:=printf}"
58 : "${cmd_SED:=sed}"
59 : "${cmd_TEST:=test}"
60 : "${cmd_TR:=tr}"
61 : "${cmd_WC:=wc}"
62 : "${cmd_XARGS:=xargs}"
63
64 ## § Implementation
65 ##
66 ## `IFS´ is made null to prevent line splitting and `LC_ALL´ is set to
67 ## the Posix (C) locale.
68
69 IFS=
70 LC_ALL=C
71
72 ## The `now´ variable simply holds the current datetime.
73
74 now="$(
75 TZ=UTC0 "${cmd_DATE}" -u '+%Y-%m-%dT%H:%M:%SZ'
76 )"
77
78 ## The `modified´ variable provides a list of changed text files.
79 ## The `--diff-filter´ excludes deleted files and the `--eol´ check is
80 ## used to exclude binary files.
81
82 modified="$(
83 "${cmd_GIT}" diff -z \
84 --cached --name-only --no-renames \
85 --diff-filter=d |
86 "${cmd_XARGS}" -0 "${cmd_GIT}" ls-files --eol -- |
87 "${cmd_SED}" '/^i[/]-text/d;s/^.* //'
88 )"
89
90 ## If no files were modified, do nothing.
91
92 if "${cmd_TEST}" -z "${modified}"
93 then :
94 exit
95 fi
96
97 ## The `makepatch´ function provides the main implementation.
98 ## It takes a blob of text (`blob´) and prints a diff that Git can
99 ## apply which makes the necessary changes.
100 ## If no changes are necessary, it returns an empty string.
101
102 makepatch () {
103
104 ## The `whatline´ variable holds the line number of the `@({U+23})´
105 ## string within the blob.
106 ## This function prints nothing unless `whatline´ holds a value.
107
108 whatline="$(
109 "${cmd_PRINTF}" '%s\n' "${blob}" |
110 "${cmd_GREP}" -n '@([#])' |
111 "${cmd_HEAD}" -n 1 |
112 "${cmd_SED}" 's/^\([^:]*\):.*$/\1/'
113 )"
114
115 ## Up to five lines following the line indicated by `whatline´ are
116 ## included as context in the resulting diff.
117
118 if "${cmd_TEST}" -n "${whatline}"
119 then :
120 context="$(
121 "${cmd_PRINTF}" '%s\n' "${blob}" |
122 "${cmd_HEAD}" -n $((${whatline} + 5))
123 )"
124 diff="$(
125
126 ## First, the diff includes lines up to `whatline´ with no
127 ## modifications, and deletes the `whatline´ line as it currently
128 ## exists.
129
130 "${cmd_PRINTF}" '%s\n' "${context}" |
131 "${cmd_SED}" \
132 -e $((${whatline} + 1))',$d' \
133 -e "${whatline}"'!s/^/ /' \
134 -e "${whatline}"'s/^/-/'
135
136 ## An updated version of the `whatline´ line is then inserted.
137
138 {
139 "${cmd_PRINTF}" '%s\n' "${context}" |
140 "${cmd_SED}" \
141 -e "${whatline}"'!d' \
142 -e "${whatline}"'s/\(@([#])[^ ]*\)\( .*\)*/\1/' \
143 -e 's/^/+/' |
144 "${cmd_TR}" -d '\n'
145 "${cmd_PRINTF}" '\t%s' "${file}" "${now}"
146 "${cmd_PRINTF}" '%s\n' ''
147 } |
148 "${cmd_SED}" 's/ *$//'
149
150 ## Finally, lines whose indices follow `whatline´ are included with·out
151 ## modifications.
152
153 "${cmd_PRINTF}" '%s\n' "${context}" |
154 "${cmd_SED}" \
155 -e '1,'"${whatline}"'d' \
156 -e 's/^/ /'
157 )"
158
159 ## The number of lines in the resulting diff cannot be assumed and
160 ## must be counted.
161 ## The diff always starts at index 1 and has a context length 1 less
162 ## than its number of lines (because the deletion and addition affect
163 ## the same line of content).
164
165 difflen="$(
166 "${cmd_PRINTF}" '%s\n' "${diff}" |
167 "${cmd_WC}" -l
168 )"
169 diffend=$((${difflen} - 1))
170
171 ## The diff is printed in a rough `diff --git´ format.
172
173 "${cmd_PRINTF}" '%s\n' \
174 'diff --git "a/'"${file}"'" "b/'"${file}"'"' \
175 '--- "a/'"${file}"'"' \
176 '+++ "b/'"${file}"'"' \
177 '@@ -1,'"${diffend}"' +1,'"${diffend}"' @@' \
178 "${diff}"
179 fi
180 }
181
182 ## Staged files are read and the diffs are applied to them when
183 ## applicable.
184 ## Files in the working directory are only updated after changes to the
185 ## index succeed.
186
187 "${cmd_PRINTF}" '%s\n' "${modified}" |
188 while read -r file
189 do :
190 indexblob="$(
191 "${cmd_GIT}" cat-file blob ':'"${file}" |
192 "${cmd_HEAD}" -n 24
193 )"
194 indexpatch="$(
195 blob="${indexblob}" makepatch
196 )"
197 if "${cmd_TEST}" -n "${indexpatch}"
198 then :
199 "${cmd_PRINTF}" '%s\n' "${indexpatch}" |
200 "${cmd_GIT}" apply --cached
201 workingblob="$(
202 "${cmd_HEAD}" -n 24 -- "${file}"
203 )"
204 workingpatch="$(
205 blob="${workingblob}" makepatch
206 )"
207 if "${cmd_TEST}" -n "${workingpatch}"
208 then :
209 "${cmd_PRINTF}" '%s\n' "${workingpatch}" |
210 "${cmd_GIT}" apply
211 fi
212 fi
213 done
This page took 0.068015 seconds and 5 git commands to generate.