Skip to content

hooks: add custom post-command hook config #736

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Documentation/config.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,8 @@ include::config/pack.adoc[]

include::config/pager.adoc[]

include::config/postcommand.adoc[]

include::config/pretty.adoc[]

include::config/promisor.adoc[]
Expand Down
13 changes: 13 additions & 0 deletions Documentation/config/postcommand.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
postCommand.strategy::
The `post-command` hook is run on every Git process by default. This
config option allows running the hook only conditionally, according
to these values:
+
----
`always`;;
run the `post-command` hook on every process (default).

`post-index-change`;;
run the `post-command` hook only if the current process wrote to
the index.
----
12 changes: 6 additions & 6 deletions Documentation/config/protocol.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ protocol.allow::
default policy of `never`, and all other protocols (including file)
have a default policy of `user`. Supported policies:
+
--
----

* `always` - protocol is always able to be used.

Expand All @@ -18,15 +18,15 @@ protocol.allow::
execute clone/fetch/push commands without user input, e.g. recursive
submodule initialization.

--
----

protocol.<name>.allow::
Set a policy to be used by protocol `<name>` with clone/fetch/push
commands. See `protocol.allow` above for the available policies.
+
The protocol names currently used by git are:
+
--
----
- `file`: any local file-based path (including `file://` URLs,
or local paths)

Expand All @@ -42,7 +42,7 @@ The protocol names currently used by git are:

- any external helpers are named by their protocol (e.g., use
`hg` to allow the `git-remote-hg` helper)
--
----

protocol.version::
If set, clients will attempt to communicate with a server
Expand All @@ -51,7 +51,7 @@ protocol.version::
If unset, the default is `2`.
Supported versions:
+
--
----

* `0` - the original wire protocol.

Expand All @@ -60,4 +60,4 @@ protocol.version::

* `2` - Wire protocol version 2, see linkgit:gitprotocol-v2[5].

--
----
4 changes: 2 additions & 2 deletions Documentation/config/push.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ push.default::
(i.e. the fetch source is equal to the push destination),
`upstream` is probably what you want. Possible values are:
+
--
----

* `nothing` - do not push anything (error out) unless a refspec is
given. This is primarily meant for people who want to
Expand Down Expand Up @@ -64,7 +64,7 @@ branches outside your control.
This used to be the default, but not since Git 2.0 (`simple` is the
new default).

--
----

push.followTags::
If set to true, enable `--follow-tags` option by default. You
Expand Down
4 changes: 2 additions & 2 deletions Documentation/config/sendemail.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ the documentation of the email program of the same name. The
differences and limitations from the standard formats are
described below:
+
--
----
sendmail;;
* Quoted aliases and quoted addresses are not supported: lines that
contain a `"` symbol are ignored.
Expand All @@ -68,7 +68,7 @@ sendmail;;
* Warnings are printed on the standard error output for any
explicitly unsupported constructs, and any other lines that are not
recognized by the parser.
--
----
sendemail.annotate::
sendemail.bcc::
sendemail.cc::
Expand Down
4 changes: 2 additions & 2 deletions Documentation/config/sideband.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ sideband.allowControlCharacters::
unwanted ANSI escape sequences from being sent to the terminal. Use
this config setting to override this behavior:
+
--
----
color::
Allow ANSI color sequences, line feeds and horizontal tabs,
but mask all other control characters. This is the default.
Expand All @@ -13,4 +13,4 @@ sideband.allowControlCharacters::
horizontal tabs.
true::
Allow all control characters to be sent to the terminal.
--
----
4 changes: 2 additions & 2 deletions Documentation/config/ssh.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ overridden via the environment variable `GIT_SSH_VARIANT`.
The current command-line parameters used for each variant are as
follows:
+
--
----

* `ssh` - [-p port] [-4] [-6] [-o option] [username@]host command

Expand All @@ -29,7 +29,7 @@ follows:

* `tortoiseplink` - [-P port] [-4] [-6] -batch [username@]host command

--
----
+
Except for the `simple` variant, command-line parameters are likely to
change as git gains new features.
8 changes: 4 additions & 4 deletions Documentation/config/status.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,11 @@ status.showUntrackedFiles::
systems. So, this variable controls how the commands display
the untracked files. Possible values are:
+
--
----
* `no` - Show no untracked files.
* `normal` - Show untracked files and directories.
* `all` - Show also individual files in untracked directories.
--
----
+
If this variable is not specified, it defaults to 'normal'.
All usual spellings for Boolean value `true` are taken as `normal`
Expand Down Expand Up @@ -90,12 +90,12 @@ status.deserializeWait::
fall-back and compute status normally. This will be overridden by
`--deserialize-wait=<value>` on the command line.
+
--
----
* `fail` - cause git to exit with an error when the status cache file
is stale; this is intended for testing and debugging.
* `block` - cause git to spin and periodically retry the cache file
every 100 ms; this is intended to help coordinate with another git
instance concurrently computing the cache file.
* `no` - to immediately fall-back if cache file is stale. This is the default.
* `<timeout>` - time (in tenths of a second) to spin and retry.
--
----
4 changes: 2 additions & 2 deletions Documentation/config/survey.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ survey.*::
command. The intention is that this command could be run in the
background with these options.
+
--
----
survey.namerev::
Boolean to show/hide `git name-rev` information for each
reported commit and the containing commit of each
Expand Down Expand Up @@ -44,4 +44,4 @@ survey.*::
long file or subdirectory entry names. Provides a
default value for `--tree-sizes=<n>` in
linkgit:git-survey[1].
--
----
2 changes: 1 addition & 1 deletion Documentation/config/trace2.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ trace2.eventTarget::
This variable controls the event target destination.
It may be overridden by the `GIT_TRACE2_EVENT` environment variable.
The following table shows possible values.
+

include::../trace2-target-values.adoc[]

trace2.normalBrief::
Expand Down
14 changes: 7 additions & 7 deletions Documentation/trace2-target-values.adoc
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
--
----
* `0` or `false` - Disables the target.
* `1` or `true` - Writes to `STDERR`.
* `[2-9]` - Writes to the already opened file descriptor.
* `<absolute-pathname>` - Writes to the file in append mode. If the target
already exists and is a directory, the traces will be written to files (one
per process) underneath the given directory.
already exists and is a directory, the traces will be written to files (one
per process) underneath the given directory.
* `af_unix:[<socket-type>:]<absolute-pathname>` - Write to a
Unix DomainSocket (on platforms that support them). Socket
type can be either `stream` or `dgram`; if omitted Git will
try both.
--
Unix DomainSocket (on platforms that support them). Socket
type can be either `stream` or `dgram`; if omitted Git will
try both.
----
1 change: 1 addition & 0 deletions git.c
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,7 @@ static int run_post_command_hook(struct repository *r)

run_post_hook = 0;
strvec_clear(&sargv);
strvec_clear(&opt.args);
setenv("COMMAND_HOOK_LOCK", "false", 1);
return ret;
}
Expand Down
75 changes: 73 additions & 2 deletions hook.c
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#define USE_THE_REPOSITORY_VARIABLE

#include "git-compat-util.h"
#include "trace2/tr2_sid.h"
#include "abspath.h"
#include "environment.h"
#include "advice.h"
Expand Down Expand Up @@ -176,6 +177,58 @@ static void run_hooks_opt_clear(struct run_hooks_opt *options)
strvec_clear(&options->args);
}

static char *get_post_index_change_sentinel_name(struct repository *r)
{
struct strbuf path = STRBUF_INIT;
const char *sid = tr2_sid_get();
char *slash = strchr(sid, '/');

/*
* Name is based on top-level SID, so children can indicate that
* the top-level process should run the post-command hook.
*/
if (slash)
*slash = 0;

repo_git_path_replace(r, &path, "hooks/index-change-%s.snt", sid);

return strbuf_detach(&path, NULL);
}

static int write_post_index_change_sentinel(struct repository *r)
{
char *path = get_post_index_change_sentinel_name(r);
FILE *fp = xfopen(path, "w");

if (fp) {
fprintf(fp, "run post-command hook");
fclose(fp);
}

free(path);
return fp ? 0 : -1;
}

/**
* Try to delete the sentinel file for this repository. If that succeeds, then
* return 1.
*/
static int post_index_change_sentinel_exists(struct repository *r)
{
char *path = get_post_index_change_sentinel_name(r);
int res = 1;

if (unlink(path)) {
if (is_missing_file_error(errno))
res = 0;
else
warning_errno("failed to remove index-change sentinel file '%s'", path);
}

free(path);
return res;
}

int run_hooks_opt(struct repository *r, const char *hook_name,
struct run_hooks_opt *options)
{
Expand All @@ -185,7 +238,7 @@ int run_hooks_opt(struct repository *r, const char *hook_name,
.hook_name = hook_name,
.options = options,
};
const char *hook_path = find_hook(r, hook_name);
const char *hook_path;
int ret = 0;
const struct run_process_parallel_opts opts = {
.tr2_category = "hook",
Expand All @@ -201,6 +254,21 @@ int run_hooks_opt(struct repository *r, const char *hook_name,
.data = &cb_data,
};

/* Interject hook behavior depending on strategy. */
if (r && r->gitdir) {
const char *strval;
if (!repo_config_get_string_tmp(r, "postcommand.strategy", &strval) &&
!strcasecmp(strval, "post-index-change")) {
if (!strcmp(hook_name, "post-index-change"))
return write_post_index_change_sentinel(r);
if (!strcmp(hook_name, "post-command") &&
!post_index_change_sentinel_exists(r))
return 0;
}
}

hook_path = find_hook(r, hook_name);

/*
* Backwards compatibility hack in VFS for Git: when originally
* introduced (and used!), it was called `post-indexchanged`, but this
Expand Down Expand Up @@ -252,12 +320,15 @@ int run_hooks_l(struct repository *r, const char *hook_name, ...)
{
struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
va_list ap;
int result;
const char *arg;

va_start(ap, hook_name);
while ((arg = va_arg(ap, const char *)))
strvec_push(&opt.args, arg);
va_end(ap);

return run_hooks_opt(r, hook_name, &opt);
result = run_hooks_opt(r, hook_name, &opt);
strvec_clear(&opt.args);
return result;
}
42 changes: 42 additions & 0 deletions t/t0401-post-command-hook.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ test_expect_success 'with succeeding hook' '
'

test_expect_success 'with failing pre-command hook' '
test_when_finished rm -f .git/hooks/pre-command &&
write_script .git/hooks/pre-command <<-EOF &&
exit 1
EOF
Expand All @@ -30,4 +31,45 @@ test_expect_success 'with failing pre-command hook' '
test_path_is_missing "$(cat .git/post-command.out)"
'

test_expect_success 'with post-index-change config' '
mkdir -p .git/hooks &&
write_script .git/hooks/post-command <<-EOF &&
echo ran >post-command.out
EOF
write_script .git/hooks/post-index-change <<-EOF &&
echo ran >post-index-change.out
EOF

# First, show expected behavior.
echo ran >expect &&
rm -f post-command.out post-index-change.out &&

# rev-parse leaves index intact, but runs post-command.
git rev-parse HEAD &&
test_path_is_missing post-index-change.out &&
test_cmp expect post-command.out &&
rm -f post-command.out &&

echo stuff >>file &&
# add updates the index and runs post-command.
git add file &&
test_cmp expect post-index-change.out &&
test_cmp expect post-command.out &&

# Now, show configured behavior
git config postCommand.strategy post-index-change &&
rm -f post-command.out post-index-change.out &&

# rev-parse leaves index intact and thus skips post-command.
git rev-parse HEAD &&
test_path_is_missing post-index-change.out &&
test_path_is_missing post-command.out &&

echo stuff >>file &&
# add updates the index and runs post-command.
git add file &&
test_path_is_missing post-index-change.out &&
test_cmp expect post-command.out
'

test_done
Loading