Subject: [PATCH] Give support for hooks based on platform

From: "Rafael Santiago via GitGitGadget" <[email-masked]>
Date: Sat, 21 Aug 2021 20:00:07 +0000

From: rafael-santiago <[email-masked]>

The idea behind this commit can be useful for teams
that share git-hooks into a custom directory and
dealing with projects that must be developed,
built, maintained on several different platforms.

This commit allows the execution of git hooks
based on the current operating system.
A "native hook" is defined in the form:
    hooks/hook-name_platform
Where platform must be equivalent to the
content returned in sysname field in utsname
struct when calling uname() [but all normalized
in lowercase].

On Windows, independent of version, flavor, SP,
whatever it is simply "windows".

When a native hook is not found the standard
hook (.git/hook/hook-name), if found is executed,
of course. In other words, the hook without a
platform postfix (_yyz) is the standard hook.
When native hook is not set as executable but
standard is set, the standard will be executed.

The main motivation of this extension is to
reduce dependency of scripting languages,
logical trinkets etc just to execute minor
tasks during scm events that could be done
natively but differently from a platform
to another. Less dependencies, cleaner
repos: a small step for a better world
for any software developer.

Signed-off-by: Rafael Santiago <[email-masked]>
---
    Give support for hooks based on platform
    
    The idea behind this commit can be useful for teams that share git-hooks
    into a custom directory and dealing with projects that must be
    developed, built, maintained on several different platforms.
    
    This commit allows the execution of git hooks based on the current
    operating system. A "native hook" is defined in the form:
    hooks/hook-name_platform
    
    Where platform must be equivalent to the content returned in sysname
    field in utsname struct when calling uname() [but all normalized in
    lowercase].
    
    On Windows, independent of version, flavor, SP, whatever it is simply
    "windows".
    
    When a native hook is not found the standard hook (.git/hook/hook-name),
    if found is executed, of course. In other words, the hook without a
    platform postfix (_yyz) is the standard hook. When native hook is not
    set as executable but standard is set, the standard will be executed.
    
    The main motivation of this extension is to reduce dependency of
    scripting languages, logical trinkets etc just to execute minor tasks
    during scm events that could be done natively but differently from a
    platform to another. Less dependencies, cleaner repos: a small step for
    a better world for any software developer.

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1069%2Frafael-santiago%2Fmaster-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1069/rafael-santiago/master-v1
Pull-Request: https://github.com/git/git/pull/1069

 run-command.c                     | 41 ++++++++++++++++++++
 t/t7527-pre-commit-native-hook.sh | 63 +++++++++++++++++++++++++++++++
 2 files changed, 104 insertions(+)
 create mode 100755 t/t7527-pre-commit-native-hook.sh

diff --git a/run-command.c b/run-command.c
index f72e72cce73..973c1a3434b 100644
--- a/run-command.c
+++ b/run-command.c
@@ -1319,9 +1319,50 @@ int async_with_fork(void)
 #endif
 }
 
+static inline const char *platform_name(void)
+{
+	static const char *platform = NULL;
+#ifndef GIT_WINDOWS_NATIVE
+	static struct utsname un = { 0 };
+#endif
+	if (platform != NULL)
+		return platform;
+
+#ifndef GIT_WINDOWS_NATIVE
+	if (uname(&un) == 0) {
+		for (size_t s = 0; un.sysname[s] != 0; s++)
+			un.sysname[s] = tolower(un.sysname[s]);
+		platform = un.sysname;
+	}
+#else
+    platform = "windows";
+#endif
+
+    return platform;
+}
+
+static const char *find_native_hook(const char *name)
+{
+	char native_name[64] = "";
+	const char *platform = NULL;
+	if (name == NULL || strstr(name, "_") != NULL)
+		return NULL;
+
+	platform = platform_name();
+	if (platform == NULL)
+		return NULL;
+
+	if (snprintf(native_name, sizeof(native_name) - 1, "%s_%s", name, platform) >= sizeof(native_name) - 1)
+		return NULL;
+	return find_hook(native_name);
+}
+
 const char *find_hook(const char *name)
 {
+	const char *native_hook = find_native_hook(name);
 	static struct strbuf path = STRBUF_INIT;
+	if (native_hook != NULL)
+		return native_hook;
 
 	strbuf_reset(&path);
 	strbuf_git_path(&path, "hooks/%s", name);
diff --git a/t/t7527-pre-commit-native-hook.sh b/t/t7527-pre-commit-native-hook.sh
new file mode 100755
index 00000000000..f3835f943af
--- /dev/null
+++ b/t/t7527-pre-commit-native-hook.sh
@@ -0,0 +1,63 @@
+#!/bin/sh
+
+test_description='Test native hooks extension'
+
+. ./test-lib.sh
+
+expected_platform=$(uname -s | tr A-Z a-z)
+
+if [ $(expr substr $(uname -s | tr A-Z a-z) 1 5) == "mingw" ] ; then
+    expected_platform="windows"
+fi
+
+test_expect_success 'set standard and native pre-commit hooks' '
+	mkdir -p test-repo &&
+	cd test-repo &&
+	git init &&
+	mkdir -p .git/hooks &&
+	echo \#!/bin/sh > .git/hooks/pre-commit &&
+	echo echo Hello generic. >> .git/hooks/pre-commit &&
+	chmod u+x .git/hooks/pre-commit &&
+	echo \#!/bin/sh > .git/hooks/pre-commit_${expected_platform} &&
+	echo echo Hello ${expected_platform} >> .git/hooks/pre-commit_${expected_platform} &&
+	chmod u+x .git/hooks/pre-commit_${expected_platform} &&
+	echo test > README &&
+	git add README &&
+	git commit -am "1-2-3 this is a test." 2>out.txt &&
+	cat out.txt | grep Hello\ ${expected_platform}
+'
+
+if [ ${expected_platform} != "windows" ] ; then
+	# chmod does not work well on Windows.
+	test_expect_success 'set standard and native pre-commit hooks but let the native one not executable' '
+		mkdir -p test-repo &&
+		cd test-repo &&
+		git init &&
+		mkdir -p .git/hooks &&
+		echo \#!/bin/sh > .git/hooks/pre-commit &&
+		echo echo Hello generic. >> .git/hooks/pre-commit &&
+		chmod u+x .git/hooks/pre-commit &&
+		echo \#!/bin/sh > .git/hooks/pre-commit_${expected_platform} &&
+		echo echo Hello ${expected_platform} >> .git/hooks/pre-commit_${expected_platform} &&
+		echo test > README &&
+		git add README &&
+		git commit -am "1-2-3 this is a test." 2>out.txt &&
+		cat out.txt | grep Hello\ generic
+	'
+
+	test_expect_success 'set standard pre-commit hook only' '
+		mkdir -p test-repo &&
+		cd test-repo &&
+		git init &&
+		mkdir -p .git/hooks &&
+		echo \#!/bin/sh > .git/hooks/pre-commit &&
+		echo echo Hello standard hook. >> .git/hooks/pre-commit &&
+		chmod u+x .git/hooks/pre-commit &&
+		echo test > README &&
+		git add README &&
+		git commit -am "1-2-3 this is a test." 2>out.txt &&
+		cat out.txt | grep Hello\ standard\ hook
+	'
+fi
+
+test_done

base-commit: 225bc32a989d7a22fa6addafd4ce7dcd04675dbf
-- 
gitgitgadget