aboutsummaryrefslogtreecommitdiffstats
path: root/git-tools/hooks/commit-msg
blob: 136606252c513a653b84901c5df7c4a6e62dc5d8 (plain)
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
Diffstat (limited to 'phpBB/phpbb/help/controller/faq.php')
0 files changed, 0 insertions, 0 deletions
href='#n145'>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 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359
#!/bin/sh
#
# A hook to check syntax of a phpBB3 commit message, per:
# * <http://wiki.phpbb.com/display/DEV/Git>
# * <http://area51.phpbb.com/phpBB/viewtopic.php?p=209919#p209919>
#
# This is a commit-msg hook.
#
# To install this you can either copy or symlink it to
# $GIT_DIR/hooks, example:
#
# ln -s ../../git-tools/hooks/commit-msg \\
#   .git/hooks/commit-msg
#
# You can configure whether invalid commit messages abort commits:
#
# git config phpbb.hooks.commit-msg.fatal true	(abort)
# git config phpbb.hooks.commit-msg.fatal false (warn only, do not abort)
#
# The default is to warn only.
#
# Warning/error messages use color by default if the output is a terminal
# ("output" here is normally standard error when you run git commit).
# To force or disable the use of color:
#
# git config phpbb.hooks.commit-msg.color true	(force color output)
# git config phpbb.hooks.commit-msg.color false (disable color output)

config_ns="phpbb.hooks.commit-msg";

if [ "$(git config --bool $config_ns.fatal)" = "true" ]
then
	fatal=1;
	severity=Error;
else
	fatal=0;
	severity=Warning;
fi

debug_level=$(git config --int $config_ns.debug || echo 0);

# Error codes
ERR_LENGTH=1;
ERR_HEADER=2;
ERR_EMPTY=3;
ERR_DESCRIPTION=4;
ERR_FOOTER=5;
ERR_EOF=6;
ERR_UNKNOWN=42;

debug()
{
	local level;

	level=$1;
	shift;

	if [ $debug_level -ge $level ]
	then
		echo $@;
	fi
}

quit()
{
	if [ $1 -eq 0 ] || [ $1 -eq $ERR_UNKNOWN ]
	then
		# success
		exit 0;
	elif [ $fatal -eq 0 ]
	then
		# problems found but fatal is false
		complain 'Please run `git commit --amend` and fix the problems mentioned.' 1>&2
		exit 0;
	else
		complain "Aborting commit." 1>&2
		exit $1;
	fi
}

use_color()
{
	if [ -z "$use_color_cached" ]
	then
		case $(git config --bool $config_ns.color)
		in
		false)
			use_color_cached=1
			;;
		true)
			use_color_cached=0
			;;
		*)
			# tty detection in shell:
			# http://hwi.ath.cx/jsh/list/shext/isatty.sh.html
			tty 0>/dev/stdout >/dev/null 2>&1
			use_color_cached=$?
			;;
		esac
	fi
	# return value is the flag inverted -
	# if return value is 0, this means use color
	return $use_color_cached
}

complain()
{
	if use_color
	then
		# Careful: our argument may include arguments to echo like -n
		# ANSI color codes:
		# http://pueblo.sourceforge.net/doc/manual/ansi_color_codes.html
		printf "\033[31m\033[1m"
		if [ "$1" = "-n" ]
		then
			echo "$@"
			printf "\033[0m"
		else
			# This will print one trailing space.
			# Not sure how to avoid this at the moment.
			echo "$@" $(printf "\033[0m")
		fi
	else
		echo "$@"
	fi
}

# Check for empty commit message
if ! grep -qv '^#' "$1"
then
	# Commit message is empty (or contains only comments).
	# Let git handle this.
	# It will abort with a message like so:
	#
	# Aborting commit due to empty commit message.
	exit 0
fi

msg=$(grep -v '^#' "$1" |grep -nE '.{81,}')

if [ $? -eq 0 ]
then
	complain "The following lines are greater than 80 characters long:" >&2;
	complain >&2
	complain "$msg" >&2;

	quit $ERR_LENGTH;
fi

lines=$(wc -l "$1" | awk '{ print $1; }');
expecting=header;
in_description=0;
in_empty=0;
ticket=0;
branch_regex="[a-z]+[a-z0-9-]*[a-z0-9]+";
i=1;
tickets="";

while [ $i -le $lines ]
do
	# Grab the line we are studying
	line=$(head -n$i "$1" | tail -n1);

	debug 1 "==> [$i] $line (description: $in_description, empty: $in_empty)";

	err=$ERR_UNKNOWN;

	if [ -z "$expecting" ]
	then
		quit $err;
	fi

	if [ "${expecting#comment}" = "$expecting" ]
	then
		# Prefix comments to the expected tokens list
		expecting="comment $expecting";
	fi

	debug 2 "Expecting: $expecting";

	# Loop over each of the expected line formats
	for expect in $expecting
	do
		# Reset the error code each iteration
		err=$ERR_UNKNOWN;

		# Test for validity of each line format
		# This is done first so $? contains the result
		case $expect in
			"header")
				err=$ERR_HEADER;
				echo "$line" | grep -Eq "^\[(ticket/[0-9]+|feature/$branch_regex|task/$branch_regex)\] .+$"
				result=$?
				if ! echo "$line" | grep -Eq "^\[(ticket/[0-9]+|feature/$branch_regex|task/$branch_regex)\] [a-zA-Z].+$"
				then
					# Don't be too strict.
					# Commits may be temporary, intended to be squashed later.
					# Just issue a warning here.
					complain "$severity: heading should be a sentence beginning with a letter." 1>&2
					complain "You entered:" 1>&2
					complain "$line" 1>&2
				fi
				# restore exit code
				(exit $result)
			;;
			"empty")
				err=$ERR_EMPTY;
				echo "$line" | grep -Eq "^$"
			;;
			"description")
				err=$ERR_DESCRIPTION;
				# Free flow text, the line length was constrained by the initial check
				echo "$line" | grep -Eq "^.+$";
			;;
			"footer")
				err=$ERR_FOOTER;
				# Each ticket is on its own line
				echo "$line" | grep -Eq "^PHPBB3-[0-9]+$";
			;;
			"eof")
				err=$ERR_EOF;
				# Should not end up here
				false
			;;
			"possibly-eof")
				# Allow empty and/or comment lines at the end
				! tail -n +"$i" "$1" |grep -qvE '^($|#)'
			;;
			"comment")
				echo "$line" | grep -Eq "^#";
			;;
			*)
				complain "Unrecognised token $expect" >&2;
				quit $err;
			;;
		esac

		# Preserve the result of the line check
		result=$?;

		debug 2 "$expect - '$line' - $result";

		if [ $result -eq 0 ]
		then
			# Break out the loop on success
			# otherwise roll on round and keep looking for a match
			break;
		fi
	done

	if [ $result -eq 0 ]
	then
		# Have we switched out of description mode?
		if [ $in_description -eq 1 ] && [ "$expect" != "description" ] && [ "$expect" != "empty" ] && [ "$expect" != "comment" ]
		then
			# Yes, okay we need to backtrace one line and reanalyse
			in_description=0;
			i=$(( $i - $in_empty ));

			# Reset the empty counter
			in_empty=0;
			continue;
		fi

		# Successful match, but on which line format
		case $expect in
			"header")
				expecting="empty";

				echo "$line" | grep -Eq "^\[ticket/[0-9]+\]$" && (
					ticket=$(echo "$line" | sed 's,\[ticket/\([0-9]*\)\].*,\1,');
				)
			;;
			"empty")
				# Description might have empty lines as spacing
				expecting="footer description";
				in_empty=$(($in_empty + 1));

				if [ $in_description -eq 1 ]
				then
					expecting="$expecting empty";
				fi
			;;
			"description")
				expecting="description empty";
				in_description=1;
			;;
			"footer")
				expecting="footer possibly-eof";
				if [ "$tickets" = "" ]
				then
					tickets="$line";
				else
					tickets="$tickets $line";
				fi
			;;
			"comment")
				# Comments should expect the same thing again
			;;
			"possibly-eof")
				expecting="eof";
			;;
			*)
				complain "Unrecognised token $expect" >&2;
				quit 254;
			;;
		esac

		if [ "$expect" != "empty" ]
		then
			in_empty=0;
		fi

		debug 3 "Now expecting: $expecting";
	else
		# None of the expected line formats matched
		# Guess we'll call it a day here then
		complain "Syntax error on line $i:" >&2;
		complain ">> $line" >&2;
		complain -n "Expecting: " >&2;
		complain "$expecting" | sed 's/ /, /g' >&2;
		quit $err;
	fi

	i=$(( $i + 1 ));
done

# If EOF is expected exit cleanly
echo "$expecting" | grep -q "eof" || (
	# Unexpected EOF, error
	complain "Unexpected EOF encountered" >&2;
	quit $ERR_EOF;
) && (
	# Do post scan checks
	if [ ! -z "$tickets" ]
	then
		# Check for duplicate tickets
		dupes=$(echo "$tickets" | sed 's/ /\n/g' | sort | uniq -d);

		if [ ! -z "$dupes" ]
		then
			complain "The following tickets are repeated:" >&2;
			complain "$dupes" | sed 's/ /\n/g;s/^/* /g' >&2;
			quit $ERR_FOOTER;
		fi
	fi
	# Check the branch ticket is mentioned, doesn't make sense otherwise
	if [ $ticket -gt 0 ]
	then
		echo "$tickets" | grep -Eq "\bPHPBB3-$ticket\b" || (
			complain "Ticket ID [$ticket] of branch missing from list of tickets:" >&2;
			complain "$tickets" | sed 's/ /\n/g;s/^/* /g' >&2;
			quit $ERR_FOOTER;
		) || exit $?;
	fi
	# Got here okay exit to reality
	exit 0;
);
exit $?;