efb8a4c030392802d7bd6406e83e8c595418820e
[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 # 'a' => 'Some Author <user@example.org>',
18 # 'd' => '<git author date>',
19 # );
20 # $rc = callScript ('git-commit', $params, $file_contents, $stdout, $stderr);
21 #
22 # The meaning of $stdout and $stderr is the same as in queryTerminal().
23 #
24 # This script uses sudo to switch between the pseudo-users and requires an
25 # entry in sudoers along the following lines:
26 # httpduser ALL=(racktablesuser) NOPASSWD:/path/to/racktables/gateways/git-commit
27
28 THISFILE=`basename "$0"`
29
30 usage_and_exit()
31 {
32 cat >&2 <<ENDOFMESSAGE
33 Usage: $THISFILE -u <u> -r <r> -o pull
34 or: $THISFILE -u <u> -r <r> -o commit -f <f> [-m <m> -a <a> -d <d>]
35 or: $THISFILE -u <u> -r <r> -o push
36 or: $THISFILE -u <u> -r <r> [-o full] -f <f> [-m <m> -a <a> -d <d>]
37 or: $THISFILE -h
38
39 -u <username> A pseudo-user to work as (this script will try to sudo
40 itself if the current user is not the same). The user
41 must be able to write to the repository filesystem and to
42 run "git pull", "git commit" and "git push" without any
43 user interaction (i.e. the git remote must be on a local
44 filesystem or be configured to use SSH keys).
45 -r <repodir> An absolute path within an existing git repository (does
46 not need to be the top directory of the repository).
47 -o pull Only run git-pull(1).
48 -o commit Only replace the file contents with stdin and run
49 git-commit(1) if the contents has changed.
50 -o push Only run git-push(1).
51 -o full This is the default and is the same as running the three
52 actions above one after another, except the push will be
53 skipped if the commit was skipped.
54 -f <filepath> A relative path to a file within the repository (if the
55 file or the path do not exist, the missing component(s)
56 will be created automatically). The path includes the
57 file name and is relative to the <repodir> above.
58 -m <msg> An optional custom commit message instead of the default
59 one. The message may be a multi-line string, in which
60 case it should follow the format recommended in the
61 "discussion" section of the git-commit(1) man page.
62 -a <author> An optional git commit author instead of the default or the
63 one previously configured with git-config(1).
64 -d <date> An optional author date for the commit, see the "date
65 formats" section of the git-commit(1) man page.
66 -h Print this message and exit.
67 ENDOFMESSAGE
68 exit ${1:-1}
69 }
70
71 assert_nonempty_option()
72 {
73 if [ -z "$2" ]; then
74 echo "$THISFILE: missing option $1" >&2
75 usage_and_exit
76 fi
77 }
78
79 git_pull_or_exit()
80 {
81 git pull --quiet || {
82 echo "$THISFILE: failed to run 'git pull' (rc=$?)" >&2
83 exit 2
84 }
85 }
86
87 git_push_or_exit()
88 {
89 git push --quiet || {
90 echo "$THISFILE: failed to run 'git push' (rc=$?)" >&2
91 exit 5
92 }
93 }
94
95 git_commit_or_exit()
96 {
97 # git processes the path to the file automatically, but the shell
98 # redirection obviously does not.
99 DIRNAME=`dirname "$FILEPATH"`
100 [ -d "$DIRNAME" ] || mkdir -p "$DIRNAME"
101
102 # New file contents is on stdin.
103 cat > "$FILEPATH" || {
104 echo "$THISFILE: failed to write new file contents, trying to roll back." >&2
105 git checkout --quiet -- "$FILEPATH" || {
106 echo "$THISFILE: failed to run 'git checkout' after a write error." >&2
107 exit 4
108 }
109 exit 3
110 }
111
112 # git-diff exits with 0 if the file is not in the repository.
113 if ! git cat-file -e HEAD:"$FILEPATH" 2>/dev/null || ! git diff --quiet -- "$FILEPATH"; then
114 git add -- "$FILEPATH" || {
115 echo "$THISFILE: failed to run 'git add'" >&2
116 exit 9
117 }
118 git commit --quiet \
119 --message="${COMMITMSG:-update $FILEPATH}" \
120 ${AUTHOR:+--author="$AUTHOR"} \
121 ${COMMITDATE:+--date="$COMMITDATE"} \
122 -- "$FILEPATH" || \
123 {
124 echo "$THISFILE: failed to run 'git commit'" >&2
125 exit 10
126 }
127 [ "$1" = "and_push" ] && git_push_or_exit
128 fi
129 }
130
131 # Both callScript() and GNU getopt support both short and long option formats.
132 # However, use of any getopt normally implies shift, which unsets the $@
133 # special parameter and makes it impossible or difficult to pass properly
134 # quoted option values to self via sudo. The getopts shell builtin (available
135 # in bash, dash and other shells) depends on its own state variables rather
136 # than shifting, but supports short options only.
137 #
138 # The only easy way to use any long options in this script would be to make
139 # the username a fixed argument, which could be tested before the getopt
140 # processing, but that would not look consistent. Hence this script uses
141 # getopts and short options for all arguments.
142
143 ONLYRUN=full
144 while getopts u:r:o:f:m:a:d:h opt; do
145 case "$opt" in
146 u)
147 SUDOUSER="$OPTARG"
148 ;;
149 r)
150 REPODIR="$OPTARG"
151 ;;
152 o)
153 ONLYRUN="$OPTARG"
154 ;;
155 f)
156 FILEPATH="$OPTARG"
157 ;;
158 m)
159 COMMITMSG="$OPTARG"
160 ;;
161 a)
162 AUTHOR="$OPTARG"
163 ;;
164 d)
165 COMMITDATE="$OPTARG"
166 ;;
167 h)
168 usage_and_exit 0
169 ;;
170 *)
171 usage_and_exit
172 esac
173 done
174
175 assert_nonempty_option -u "$SUDOUSER"
176 [ `whoami` = "$SUDOUSER" ] || {
177 sudo --non-interactive --set-home --user="$SUDOUSER" -- "$0" "$@"
178 exit $?
179 }
180
181 assert_nonempty_option -r "$REPODIR"
182 # Do not suppress the error message from cd, which may be more useful
183 # (e.g. permission denied) than a hard-coded default message.
184 cd "$REPODIR" || exit 6
185 `which git >/dev/null` || {
186 echo "$THISFILE: git is not available" >&2
187 exit 7
188 }
189 INTREE=`git rev-parse --is-inside-work-tree 2>/dev/null`
190 [ "$INTREE" = 'true' ] || {
191 echo "$THISFILE: the directory '$REPODIR' exists, but is not within a git repository" >&2
192 exit 8
193 }
194
195 case "$ONLYRUN" in
196 pull)
197 git_pull_or_exit
198 ;;
199 commit)
200 assert_nonempty_option -f "$FILEPATH"
201 git_commit_or_exit
202 ;;
203 push)
204 git_push_or_exit
205 ;;
206 full)
207 assert_nonempty_option -f "$FILEPATH"
208 git_pull_or_exit
209 git_commit_or_exit and_push
210 ;;
211 *)
212 usage_and_exit
213 ;;
214 esac
215
216 exit 0