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