be3786b9443d514d6c611e662064d3e9860fb934
[racktables] / gateways / git-commit
1 #!/bin/sh
2
3 # This file is a part of RackTables, a datacenter and server room management
4 # framework. See accompanying file "COPYING" for the full copyright and
5 # licensing information.
6
7 # This script implements a simple (one file at a time) one-way feed into a git
8 # repository. To make a commit it takes the following PHP code:
9 #
10 # $params = array
11 # (
12 # 'u' => 'racktables_pseudo_user',
13 # 'r' => '/path/to/repository',
14 # 'o' => 'pull', # or 'commit' or 'push' or unset
15 # 'f' => 'path/to/file/within/the/repository/file.txt',
16 # 'm' => 'commit message text',
17 # 'M' => 'append',
18 # 'a' => 'Some Author <user@example.org>',
19 # 'd' => '<git author date>',
20 # 'v' => 'normal',
21 # );
22 # $rc = callScript ('git-commit', $params, $file_contents, $stdout, $stderr);
23 #
24 # The meaning of $stdout and $stderr is the same as in queryTerminal().
25 #
26 # This script uses sudo to switch between the pseudo-users and requires an
27 # entry in sudoers along the following lines:
28 # httpduser ALL=(racktablesuser) NOPASSWD:/path/to/racktables/gateways/git-commit
29
30 THISFILE=`basename "$0"`
31
32 print_usage_and_exit_0()
33 {
34 cat <<ENDOFMESSAGE
35 Usage: $THISFILE -u <u> -r <r> -o pull [-v <v>]
36 or: $THISFILE -u <u> -r <r> -o commit -f <f> [<commit-options>] [-v <v>]
37 or: $THISFILE -u <u> -r <r> -o push [-v <v>]
38 or: $THISFILE -u <u> -r <r> [-o full] -f <f> [<commit-options>] [-v <v>]
39 or: $THISFILE -h
40
41 Commit options: [-m <msg> [-M <M>]] [-a <author>] [-d <date>]
42
43 -u <username> A pseudo-user to work as (this script will try to sudo
44 itself if the current user is not the same). The user
45 must be able to write to the repository filesystem and to
46 run "git pull" and "git push" without any user
47 interaction (i.e. the git remote must be on a local
48 filesystem or be configured to use SSH keys).
49 -r <repodir> An absolute path within an existing git repository (does
50 not need to be the top directory of the repository).
51 -o pull Only run git-pull(1).
52 -o commit Only replace the file contents with stdin and run
53 git-commit(1) if the contents has changed.
54 -o push Only run git-push(1).
55 -o full This is the default and is the same as running the three
56 actions above one after another, except the push will be
57 skipped if the commit was skipped.
58 -f <filepath> A relative path to a file within the repository (if the
59 file or the path do not exist, the missing component(s)
60 will be created automatically). The path includes the
61 file name and is relative to the <repodir> above.
62 -m <msg> An optional custom commit message instead of the default
63 one. The message may be a multi-line string, in which
64 case it should follow the format recommended in the
65 "discussion" section of the git-commit(1) man page.
66 -M replace This is the default. If -m <msg> is specified and is not an
67 empty string, <msg> will be used as the full commit
68 message (both the one-line summary and the multi-line
69 description, if present) instead of the default
70 single-line summary.
71 -M append If -m <msg> is specified, <msg> will be appended to the
72 default single-line summary. This will extend the summary
73 and/or add a description (supposedly with an empty line
74 between the two), given proper amount and placement of
75 newlines in <msg>.
76 -a <author> An optional git commit author instead of the default or the
77 one previously configured with git-config(1).
78 -d <date> An optional author date for the commit, see the "date
79 formats" section of the git-commit(1) man page.
80 -v quiet This is the default. Run the requested git command(s) with
81 --quiet. Any errors will still be printed to stderr.
82 -v verbose Run the requested git command(s) with --verbose.
83 -v normal Run the requested git command(s) without --quiet and
84 without --verbose.
85 -h Print this message and exit.
86 ENDOFMESSAGE
87 exit 0
88 }
89
90 mention_usage_and_exit_1()
91 {
92 echo "Try '$THISFILE -h' for more information." >&2
93 exit 1
94 }
95
96 assert_nonempty_option()
97 {
98 if [ -z "$2" ]; then
99 echo "$THISFILE: missing option $1" >&2
100 mention_usage_and_exit_1
101 fi
102 }
103
104 git_pull_or_exit()
105 {
106 git pull $VLEVEL || {
107 echo "$THISFILE: failed to run 'git pull' (rc=$?)" >&2
108 exit 2
109 }
110 }
111
112 git_push_or_exit()
113 {
114 git push $VLEVEL || {
115 echo "$THISFILE: failed to run 'git push' (rc=$?)" >&2
116 exit 5
117 }
118 }
119
120 git_commit_or_exit()
121 {
122 assert_nonempty_option -f "$FILEPATH"
123 REALPATH=`realpath --canonicalize-missing --relative-to="$REPODIR" "$FILEPATH"`
124 if [ "$REALPATH" != "${REALPATH#../}" ]; then
125 echo "$THISFILE: file path '$FILEPATH' is outside of the repository directory '$REPODIR'" >&2
126 exit 12
127 fi
128 # git processes the path to the file automatically, but the shell
129 # redirection obviously does not.
130 DIRNAME=`dirname "$FILEPATH"`
131 if [ ! -d "$DIRNAME" ]; then
132 mkdir -p "$DIRNAME" || {
133 echo "$THISFILE: failed to create missing directory '$DIRNAME'" >&2
134 exit 11
135 }
136 fi
137
138 # New file contents is on stdin.
139 cat > "$FILEPATH" || {
140 echo "$THISFILE: failed to write new file contents, trying to roll back." >&2
141 git checkout --quiet -- "$FILEPATH" || {
142 echo "$THISFILE: failed to run 'git checkout' after a write error." >&2
143 exit 4
144 }
145 exit 3
146 }
147
148 # git-diff exits with 0 if the file is not in the repository.
149 if ! git cat-file -e HEAD:"$FILEPATH" 2>/dev/null || ! git diff --quiet -- "$FILEPATH"; then
150 git add -- "$FILEPATH" || {
151 echo "$THISFILE: failed to run 'git add'" >&2
152 exit 9
153 }
154 local message="update $FILEPATH"
155 # getopts validates the value of MESSAGEMODE.
156 case "$MESSAGEMODE" in
157 replace)
158 [ -n "$COMMITMSG" ] && message="$COMMITMSG"
159 ;;
160 append)
161 # No space in between -- the appended part may skip straight to
162 # the commit description.
163 message="$message$COMMITMSG"
164 ;;
165 esac
166 git commit $VLEVEL \
167 --message="$message" \
168 ${AUTHOR:+--author="$AUTHOR"} \
169 ${COMMITDATE:+--date="$COMMITDATE"} \
170 -- "$FILEPATH" || \
171 {
172 echo "$THISFILE: failed to run 'git commit'" >&2
173 exit 10
174 }
175 [ "$1" = "and_push" ] && git_push_or_exit
176 fi
177 }
178
179 # Both callScript() and GNU getopt support both short and long option formats.
180 # However, use of any getopt normally implies shift, which unsets the $@
181 # special parameter and makes it impossible or difficult to pass properly
182 # quoted option values to self via sudo. The getopts shell builtin (available
183 # in bash, dash and other shells) depends on its own state variables rather
184 # than shifting, but supports short options only.
185 #
186 # The only easy way to use any long options in this script would be to make
187 # the username a fixed argument, which could be tested before the getopt
188 # processing, but that would not look consistent. Hence this script uses
189 # getopts and short options for all arguments.
190
191 ONLYRUN=full
192 VLEVEL='--quiet'
193 MESSAGEMODE=replace
194 while getopts u:r:o:f:m:M:a:d:v:h opt; do
195 case "$opt" in
196 u)
197 SUDOUSER="$OPTARG"
198 ;;
199 r)
200 REPODIR="$OPTARG"
201 ;;
202 o)
203 case "$OPTARG" in
204 pull|commit|push|full)
205 ONLYRUN="$OPTARG"
206 ;;
207 *)
208 echo "$THISFILE: '$OPTARG' is not a valid value for -$opt" >&2
209 mention_usage_and_exit_1
210 ;;
211 esac
212 ;;
213 f)
214 FILEPATH="$OPTARG"
215 ;;
216 m)
217 COMMITMSG="$OPTARG"
218 ;;
219 M)
220 case "$OPTARG" in
221 replace|append)
222 MESSAGEMODE="$OPTARG"
223 ;;
224 *)
225 echo "$THISFILE: '$OPTARG' is not a valid value for -$opt" >&2
226 mention_usage_and_exit_1
227 ;;
228 esac
229 ;;
230 a)
231 AUTHOR="$OPTARG"
232 ;;
233 d)
234 COMMITDATE="$OPTARG"
235 ;;
236 v)
237 case "$OPTARG" in
238 quiet)
239 VLEVEL='--quiet'
240 ;;
241 normal)
242 VLEVEL=
243 ;;
244 verbose)
245 VLEVEL='--verbose'
246 ;;
247 *)
248 echo "$THISFILE: '$OPTARG' is not a valid value for -$opt" >&2
249 mention_usage_and_exit_1
250 ;;
251 esac
252 ;;
253 h)
254 print_usage_and_exit_0
255 ;;
256 *)
257 mention_usage_and_exit_1
258 ;;
259 esac
260 done
261
262 assert_nonempty_option -u "$SUDOUSER"
263 [ `whoami` = "$SUDOUSER" ] || {
264 sudo --non-interactive --set-home --user="$SUDOUSER" -- "$0" "$@"
265 exit $?
266 }
267
268 assert_nonempty_option -r "$REPODIR"
269 # Do not suppress the error message from cd, which may be more useful
270 # (e.g. permission denied) than a hard-coded default message.
271 cd "$REPODIR" || exit 6
272 `which git >/dev/null` || {
273 echo "$THISFILE: git is not available" >&2
274 exit 7
275 }
276 INTREE=`git rev-parse --is-inside-work-tree 2>/dev/null`
277 [ "$INTREE" = 'true' ] || {
278 echo "$THISFILE: the directory '$REPODIR' exists, but is not within a git repository" >&2
279 exit 8
280 }
281
282 # The getopts loop above validates the value of ONLYRUN.
283 case "$ONLYRUN" in
284 pull)
285 git_pull_or_exit
286 ;;
287 commit)
288 git_commit_or_exit
289 ;;
290 push)
291 git_push_or_exit
292 ;;
293 full)
294 git_pull_or_exit
295 git_commit_or_exit and_push
296 ;;
297 esac
298
299 exit 0