aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Documentation/RelNotes/2.54.0.adoc11
-rw-r--r--Documentation/config/format.adoc5
-rw-r--r--Documentation/config/hook.adoc30
-rw-r--r--Documentation/git-backfill.adoc5
-rw-r--r--Documentation/git-format-patch.adoc20
-rw-r--r--Documentation/git-hook.adoc27
-rw-r--r--Documentation/git-replay.adoc52
-rw-r--r--Documentation/pretty-formats.adoc4
-rw-r--r--Makefile1
-rw-r--r--builtin/backfill.c22
-rw-r--r--builtin/hook.c61
-rw-r--r--builtin/log.c89
-rw-r--r--builtin/receive-pack.c64
-rw-r--r--builtin/replay.c36
-rw-r--r--builtin/worktree.c21
-rw-r--r--hook.c190
-rw-r--r--hook.h34
-rw-r--r--path-walk.c43
-rw-r--r--path.c2
-rw-r--r--path.h6
-rw-r--r--pretty.c15
-rw-r--r--refs.c3
-rw-r--r--replay.c157
-rw-r--r--replay.h11
-rw-r--r--revision.h1
-rw-r--r--sequencer.c78
-rw-r--r--sequencer.h13
-rw-r--r--string-list.c9
-rw-r--r--string-list.h8
-rwxr-xr-xt/t1800-hook.sh167
-rwxr-xr-xt/t2400-worktree-add.sh28
-rwxr-xr-xt/t3650-replay-basics.sh111
-rwxr-xr-xt/t4014-format-patch.sh125
-rwxr-xr-xt/t5620-backfill.sh211
-rwxr-xr-xt/t8003-blame-corner-cases.sh75
-rw-r--r--transport.c3
-rw-r--r--worktree.c10
-rw-r--r--worktree.h3
38 files changed, 1365 insertions, 386 deletions
diff --git a/Documentation/RelNotes/2.54.0.adoc b/Documentation/RelNotes/2.54.0.adoc
index 4ce30d9d5b..629e603f43 100644
--- a/Documentation/RelNotes/2.54.0.adoc
+++ b/Documentation/RelNotes/2.54.0.adoc
@@ -109,6 +109,16 @@ UI, Workflows & Features
* "git repo info -h" and "git repo structure -h" limit their help output
to the part that is specific to the subcommand.
+ * "git format-patch --cover-letter" learns to use a simpler format
+ instead of the traditional shortlog format to list its commits with
+ a new --commit-list-format option and format.commitListFormat
+ configuration variable.
+
+ * `git backfill` learned to accept revision and pathspec arguments.
+
+ * "git replay" (experimental) learns, in addition to "pick" and
+ "replay", a new operating mode "revert".
+
Performance, Internal Implementation, Development Support etc.
--------------------------------------------------------------
@@ -495,3 +505,4 @@ Fixes since v2.53
(merge fc8a4f15e7 gi/doc-boolean-config-typofix later to maint).
(merge 37182267a0 kh/doc-interpret-trailers-1 later to maint).
(merge f64c50e768 jc/rerere-modern-strbuf-handling later to maint).
+ (merge 699248d89e th/t8003-unhide-git-failures later to maint).
diff --git a/Documentation/config/format.adoc b/Documentation/config/format.adoc
index ab0710e86a..dbd186290b 100644
--- a/Documentation/config/format.adoc
+++ b/Documentation/config/format.adoc
@@ -101,6 +101,11 @@ format.coverLetter::
generate a cover-letter only when there's more than one patch.
Default is false.
+format.commitListFormat::
+ When the `--cover-letter-format` option is not given, `format-patch`
+ uses the value of this variable to decide how to format the entry of
+ each commit. Defaults to `shortlog`.
+
format.outputDirectory::
Set a custom directory to store the resulting files instead of the
current working directory. All directory components will be created.
diff --git a/Documentation/config/hook.adoc b/Documentation/config/hook.adoc
index 64e845a260..9e78f26439 100644
--- a/Documentation/config/hook.adoc
+++ b/Documentation/config/hook.adoc
@@ -1,23 +1,23 @@
-hook.<name>.command::
- The command to execute for `hook.<name>`. `<name>` is a unique
- "friendly" name that identifies this hook. (The hook events that
- trigger the command are configured with `hook.<name>.event`.) The
- value can be an executable path or a shell oneliner. If more than
- one value is specified for the same `<name>`, only the last value
- parsed is used. See linkgit:git-hook[1].
+hook.<friendly-name>.command::
+ The command to execute for `hook.<friendly-name>`. `<friendly-name>`
+ is a unique name that identifies this hook. The hook events that
+ trigger the command are configured with `hook.<friendly-name>.event`.
+ The value can be an executable path or a shell oneliner. If more than
+ one value is specified for the same `<friendly-name>`, only the last
+ value parsed is used. See linkgit:git-hook[1].
-hook.<name>.event::
- The hook events that trigger `hook.<name>`. The value is the name
- of a hook event, like "pre-commit" or "update". (See
+hook.<friendly-name>.event::
+ The hook events that trigger `hook.<friendly-name>`. The value is the
+ name of a hook event, like "pre-commit" or "update". (See
linkgit:githooks[5] for a complete list of hook events.) On the
- specified event, the associated `hook.<name>.command` is executed.
- This is a multi-valued key. To run `hook.<name>` on multiple
+ specified event, the associated `hook.<friendly-name>.command` is executed.
+ This is a multi-valued key. To run `hook.<friendly-name>` on multiple
events, specify the key more than once. An empty value resets
the list of events, clearing any previously defined events for
- `hook.<name>`. See linkgit:git-hook[1].
+ `hook.<friendly-name>`. See linkgit:git-hook[1].
-hook.<name>.enabled::
- Whether the hook `hook.<name>` is enabled. Defaults to `true`.
+hook.<friendly-name>.enabled::
+ Whether the hook `hook.<friendly-name>` is enabled. Defaults to `true`.
Set to `false` to disable the hook without removing its
configuration. This is particularly useful when a hook is defined
in a system or global config file and needs to be disabled for a
diff --git a/Documentation/git-backfill.adoc b/Documentation/git-backfill.adoc
index b8394dcf22..246ab417c2 100644
--- a/Documentation/git-backfill.adoc
+++ b/Documentation/git-backfill.adoc
@@ -63,9 +63,12 @@ OPTIONS
current sparse-checkout. If the sparse-checkout feature is enabled,
then `--sparse` is assumed and can be disabled with `--no-sparse`.
+You may also specify the commit limiting options from linkgit:git-rev-list[1].
+
SEE ALSO
--------
-linkgit:git-clone[1].
+linkgit:git-clone[1],
+linkgit:git-rev-list[1]
GIT
---
diff --git a/Documentation/git-format-patch.adoc b/Documentation/git-format-patch.adoc
index 36146006fa..5662382450 100644
--- a/Documentation/git-format-patch.adoc
+++ b/Documentation/git-format-patch.adoc
@@ -24,6 +24,7 @@ SYNOPSIS
[(--reroll-count|-v) <n>]
[--to=<email>] [--cc=<email>]
[--[no-]cover-letter] [--quiet]
+ [--commit-list-format=<format-spec>]
[--[no-]encode-email-headers]
[--no-notes | --notes[=<ref>]]
[--interdiff=<previous>]
@@ -318,9 +319,21 @@ feeding the result to `git send-email`.
--cover-letter::
--no-cover-letter::
- In addition to the patches, generate a cover letter file
- containing the branch description, shortlog and the overall diffstat. You can
- fill in a description in the file before sending it out.
+ In addition to the patches, generate a cover letter file containing the
+ branch description, commit list and the overall diffstat. You can fill
+ in a description in the file before sending it out.
+
+--commit-list-format=<format-spec>::
+ Specify the format in which to generate the commit list of the patch
+ series. The accepted values for format-spec are `shortlog`, `modern` or
+ a format-string prefixed with `log:`. E.g. `log: %s (%an)`.
+ `modern` is the same as `log:%w(72)[%(count)/%(total)] %s`.
+ The `log:` prefix can be omitted if the format-string has a `%` in it
+ (expecting that it is part of `%<placeholder>`).
+ Defaults to the `format.commitListFormat` configuration variable, if
+ set, or `shortlog`.
+ This option given from the command-line implies the use of
+ `--cover-letter` unless `--no-cover-letter` is given.
--encode-email-headers::
--no-encode-email-headers::
@@ -453,6 +466,7 @@ with configuration variables.
signOff = true
outputDirectory = <directory>
coverLetter = auto
+ commitListFormat = shortlog
coverFromDescription = auto
------------
diff --git a/Documentation/git-hook.adoc b/Documentation/git-hook.adoc
index 12d2701b52..318c637bd8 100644
--- a/Documentation/git-hook.adoc
+++ b/Documentation/git-hook.adoc
@@ -8,8 +8,8 @@ git-hook - Run git hooks
SYNOPSIS
--------
[verse]
-'git hook' run [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]
-'git hook' list [-z] <hook-name>
+'git hook' run [--allow-unknown-hook-name] [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]
+'git hook' list [--allow-unknown-hook-name] [-z] [--show-scope] <hook-name>
DESCRIPTION
-----------
@@ -44,7 +44,7 @@ event`), and then `~/bin/spellchecker` will have a chance to check your commit
message (during the `commit-msg` hook event).
Commands are run in the order Git encounters their associated
-`hook.<name>.event` configs during the configuration parse (see
+`hook.<friendly-name>.event` configs during the configuration parse (see
linkgit:git-config[1]). Although multiple `hook.linter.event` configs can be
added, only one `hook.linter.command` event is valid - Git uses "last-one-wins"
to determine which command to run.
@@ -76,10 +76,10 @@ first start `~/bin/linter --cpp20` and second start `~/bin/leak-detector`. It
would evaluate the output of each when deciding whether to proceed with the
commit.
-For a full list of hook events which you can set your `hook.<name>.event` to,
+For a full list of hook events which you can set your `hook.<friendly-name>.event` to,
and how hooks are invoked during those events, see linkgit:githooks[5].
-Git will ignore any `hook.<name>.event` that specifies an event it doesn't
+Git will ignore any `hook.<friendly-name>.event` that specifies an event it doesn't
recognize. This is intended so that tools which wrap Git can use the hook
infrastructure to run their own hooks; see "WRAPPERS" for more guidance.
@@ -113,7 +113,7 @@ Any positional arguments to the hook should be passed after a
mandatory `--` (or `--end-of-options`, see linkgit:gitcli[7]). See
linkgit:githooks[5] for arguments hooks might expect (if any).
-list [-z]::
+list [-z] [--show-scope]::
Print a list of hooks which will be run on `<hook-name>` event. If no
hooks are configured for that event, print a warning and return 1.
Use `-z` to terminate output lines with NUL instead of newlines.
@@ -121,6 +121,13 @@ list [-z]::
OPTIONS
-------
+--allow-unknown-hook-name::
+ By default `git hook run` and `git hook list` will bail out when
+ `<hook-name>` is not a hook event known to Git (see linkgit:githooks[5]
+ for the list of known hooks). This is meant to help catch typos
+ such as `prereceive` when `pre-receive` was intended. Pass this
+ flag to allow unknown hook names.
+
--to-stdin::
For "run"; specify a file which will be streamed into the
hook's stdin. The hook will receive the entire file from
@@ -134,6 +141,12 @@ OPTIONS
-z::
Terminate "list" output lines with NUL instead of newlines.
+--show-scope::
+ For "list"; prefix each configured hook's friendly name with a
+ tab-separated config scope (e.g. `local`, `global`, `system`),
+ mirroring the output style of `git config --show-scope`. Traditional
+ hooks from the hookdir are unaffected.
+
WRAPPERS
--------
@@ -153,7 +166,7 @@ Then, in your 'mywrapper' tool, you can invoke any users' configured hooks by
running:
----
-git hook run mywrapper-start-tests \
+git hook run --allow-unknown-hook-name mywrapper-start-tests \
# providing something to stdin
--stdin some-tempfile-123 \
# execute hooks in serial
diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
index 8d696ce3ab..997097e420 100644
--- a/Documentation/git-replay.adoc
+++ b/Documentation/git-replay.adoc
@@ -9,7 +9,7 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t
SYNOPSIS
--------
[verse]
-(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>
+(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>
DESCRIPTION
-----------
@@ -42,6 +42,25 @@ The history is replayed on top of the <branch> and <branch> is updated to
point at the tip of the resulting history. This is different from `--onto`,
which uses the target only as a starting point without updating it.
+--revert <branch>::
+ Starting point at which to create the reverted commits; must be a
+ branch name.
++
+When `--revert` is specified, the commits in the revision range are reverted
+(their changes are undone) and the reverted commits are created on top of
+<branch>. The <branch> is then updated to point at the new commits. This is
+the same as running `git revert <revision-range>` but does not update the
+working tree.
++
+The commit messages follow `git revert` conventions: they are prefixed with
+"Revert" and include "This reverts commit <hash>." When reverting a commit
+whose message starts with "Revert", the new message uses "Reapply" instead.
+Unlike cherry-pick which preserves the original author, revert commits use
+the current user as the author, matching the behavior of `git revert`.
++
+This option is mutually exclusive with `--onto` and `--advance`. It is also
+incompatible with `--contained` (which is a modifier for `--onto` only).
+
--contained::
Update all branches that point at commits in
<revision-range>. Requires `--onto`.
@@ -60,10 +79,11 @@ The default mode can be configured via the `replay.refAction` configuration vari
<revision-range>::
Range of commits to replay; see "Specifying Ranges" in
- linkgit:git-rev-parse[1]. In `--advance <branch>` mode, the
- range should have a single tip, so that it's clear to which tip the
- advanced <branch> should point. Any commits in the range whose
- changes are already present in the branch the commits are being
+ linkgit:git-rev-parse[1]. In `--advance <branch>` or
+ `--revert <branch>` mode, the range should have a single tip,
+ so that it's clear to which tip the advanced or reverted
+ <branch> should point. Any commits in the range whose changes
+ are already present in the branch the commits are being
replayed onto will be dropped.
:git-replay: 1
@@ -84,9 +104,10 @@ When using `--ref-action=print`, the output is usable as input to
update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
where the number of refs updated depends on the arguments passed and
-the shape of the history being replayed. When using `--advance`, the
-number of refs updated is always one, but for `--onto`, it can be one
-or more (rebasing multiple branches simultaneously is supported).
+the shape of the history being replayed. When using `--advance` or
+`--revert`, the number of refs updated is always one, but for `--onto`,
+it can be one or more (rebasing multiple branches simultaneously is
+supported).
There is no stderr output on conflicts; see the <<exit-status,EXIT
STATUS>> section below.
@@ -152,6 +173,21 @@ all commits they have since `base`, playing them on top of
`origin/main`. These three branches may have commits on top of `base`
that they have in common, but that does not need to be the case.
+To revert commits on a branch:
+
+------------
+$ git replay --revert main topic~2..topic
+------------
+
+This reverts the last two commits from `topic`, creating revert commits on
+top of `main`, and updates `main` to point at the result. This is useful when
+commits from `topic` were previously merged or cherry-picked into `main` and
+need to be undone.
+
+NOTE: For reverting an entire merge request as a single commit (rather than
+commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE`
+which can avoid unnecessary merge conflicts.
+
GIT
---
Part of the linkgit:git[1] suite
diff --git a/Documentation/pretty-formats.adoc b/Documentation/pretty-formats.adoc
index 5405e57a60..2ae0eb11a9 100644
--- a/Documentation/pretty-formats.adoc
+++ b/Documentation/pretty-formats.adoc
@@ -253,6 +253,10 @@ The placeholders are:
linkgit:git-rev-list[1])
+%d+:: ref names, like the --decorate option of linkgit:git-log[1]
+%D+:: ref names without the " (", ")" wrapping.
++%(count)+:: the number of a patch within a patch series. Used only in
+ `--commit-list-format` in `format-patch`
++%(total)+:: the total number of patches in a patch series. Used only in
+ `--commit-list-format` in `format-patch`
++%(decorate++`[:<option>,...]`++)++::
ref names with custom decorations. The `decorate` string may be followed by a
colon and zero or more comma-separated options. Option values may contain
diff --git a/Makefile b/Makefile
index dbf0022054..5d22394c2e 100644
--- a/Makefile
+++ b/Makefile
@@ -2675,6 +2675,7 @@ git$X: git.o GIT-LDFLAGS $(BUILTIN_OBJS) $(GITLIBS)
help.sp help.s help.o: command-list.h
builtin/bugreport.sp builtin/bugreport.s builtin/bugreport.o: hook-list.h
+builtin/hook.sp builtin/hook.s builtin/hook.o: hook-list.h
builtin/help.sp builtin/help.s builtin/help.o: config-list.h GIT-PREFIX
builtin/help.sp builtin/help.s builtin/help.o: EXTRA_CPPFLAGS = \
diff --git a/builtin/backfill.c b/builtin/backfill.c
index e9a33e81be..2c5ce56fb7 100644
--- a/builtin/backfill.c
+++ b/builtin/backfill.c
@@ -35,6 +35,7 @@ struct backfill_context {
struct oid_array current_batch;
size_t min_batch_size;
int sparse;
+ struct rev_info revs;
};
static void backfill_context_clear(struct backfill_context *ctx)
@@ -79,7 +80,6 @@ static int fill_missing_blobs(const char *path UNUSED,
static int do_backfill(struct backfill_context *ctx)
{
- struct rev_info revs;
struct path_walk_info info = PATH_WALK_INFO_INIT;
int ret;
@@ -91,13 +91,14 @@ static int do_backfill(struct backfill_context *ctx)
}
}
- repo_init_revisions(ctx->repo, &revs, "");
- handle_revision_arg("HEAD", &revs, 0, 0);
+ /* Walk from HEAD if otherwise unspecified. */
+ if (!ctx->revs.pending.nr)
+ add_head_to_pending(&ctx->revs);
info.blobs = 1;
info.tags = info.commits = info.trees = 0;
- info.revs = &revs;
+ info.revs = &ctx->revs;
info.path_fn = fill_missing_blobs;
info.path_fn_data = ctx;
@@ -108,7 +109,6 @@ static int do_backfill(struct backfill_context *ctx)
download_batch(ctx);
path_walk_info_clear(&info);
- release_revisions(&revs);
return ret;
}
@@ -120,6 +120,7 @@ int cmd_backfill(int argc, const char **argv, const char *prefix, struct reposit
.current_batch = OID_ARRAY_INIT,
.min_batch_size = 50000,
.sparse = 0,
+ .revs = REV_INFO_INIT,
};
struct option options[] = {
OPT_UNSIGNED(0, "min-batch-size", &ctx.min_batch_size,
@@ -134,7 +135,15 @@ int cmd_backfill(int argc, const char **argv, const char *prefix, struct reposit
builtin_backfill_usage, options);
argc = parse_options(argc, argv, prefix, options, builtin_backfill_usage,
- 0);
+ PARSE_OPT_KEEP_UNKNOWN_OPT |
+ PARSE_OPT_KEEP_ARGV0 |
+ PARSE_OPT_KEEP_DASHDASH);
+
+ repo_init_revisions(repo, &ctx.revs, prefix);
+ argc = setup_revisions(argc, argv, &ctx.revs, NULL);
+
+ if (argc > 1)
+ die(_("unrecognized argument: %s"), argv[1]);
repo_config(repo, git_default_config, NULL);
@@ -143,5 +152,6 @@ int cmd_backfill(int argc, const char **argv, const char *prefix, struct reposit
result = do_backfill(&ctx);
backfill_context_clear(&ctx);
+ release_revisions(&ctx.revs);
return result;
}
diff --git a/builtin/hook.c b/builtin/hook.c
index 83020dfb4f..c0585587e5 100644
--- a/builtin/hook.c
+++ b/builtin/hook.c
@@ -4,14 +4,22 @@
#include "environment.h"
#include "gettext.h"
#include "hook.h"
+#include "hook-list.h"
#include "parse-options.h"
-#include "strvec.h"
-#include "abspath.h"
#define BUILTIN_HOOK_RUN_USAGE \
- N_("git hook run [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]")
+ N_("git hook run [--allow-unknown-hook-name] [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]")
#define BUILTIN_HOOK_LIST_USAGE \
- N_("git hook list [-z] <hook-name>")
+ N_("git hook list [--allow-unknown-hook-name] [-z] [--show-scope] <hook-name>")
+
+static int is_known_hook(const char *name)
+{
+ const char **p;
+ for (p = hook_name_list; *p; p++)
+ if (!strcmp(*p, name))
+ return 1;
+ return 0;
+}
static const char * const builtin_hook_usage[] = {
BUILTIN_HOOK_RUN_USAGE,
@@ -35,11 +43,17 @@ static int list(int argc, const char **argv, const char *prefix,
struct string_list_item *item;
const char *hookname = NULL;
int line_terminator = '\n';
+ int show_scope = 0;
+ int allow_unknown = 0;
int ret = 0;
struct option list_options[] = {
OPT_SET_INT('z', NULL, &line_terminator,
N_("use NUL as line terminator"), '\0'),
+ OPT_BOOL(0, "show-scope", &show_scope,
+ N_("show the config scope that defined each hook")),
+ OPT_BOOL(0, "allow-unknown-hook-name", &allow_unknown,
+ N_("allow running a hook with a non-native hook name")),
OPT_END(),
};
@@ -51,15 +65,22 @@ static int list(int argc, const char **argv, const char *prefix,
* arguments later they probably should be caught by parse_options.
*/
if (argc != 1)
- usage_msg_opt(_("You must specify a hook event name to list."),
+ usage_msg_opt(_("you must specify a hook event name to list"),
builtin_hook_list_usage, list_options);
hookname = argv[0];
+ if (!allow_unknown && !is_known_hook(hookname)) {
+ error(_("unknown hook event '%s';\n"
+ "use --allow-unknown-hook-name to allow non-native hook names"),
+ hookname);
+ return 1;
+ }
+
head = list_hooks(repo, hookname, NULL);
if (!head->nr) {
- warning(_("No hooks found for event '%s'"), hookname);
+ warning(_("no hooks found for event '%s'"), hookname);
ret = 1; /* no hooks found */
goto cleanup;
}
@@ -71,16 +92,27 @@ static int list(int argc, const char **argv, const char *prefix,
case HOOK_TRADITIONAL:
printf("%s%c", _("hook from hookdir"), line_terminator);
break;
- case HOOK_CONFIGURED:
- printf("%s%c", h->u.configured.friendly_name, line_terminator);
+ case HOOK_CONFIGURED: {
+ const char *name = h->u.configured.friendly_name;
+ const char *scope = show_scope ?
+ config_scope_name(h->u.configured.scope) : NULL;
+ if (scope)
+ printf("%s\t%s%s%c", scope,
+ h->u.configured.disabled ? "disabled\t" : "",
+ name, line_terminator);
+ else
+ printf("%s%s%c",
+ h->u.configured.disabled ? "disabled\t" : "",
+ name, line_terminator);
break;
+ }
default:
BUG("unknown hook kind");
}
}
cleanup:
- hook_list_clear(head, NULL);
+ string_list_clear_func(head, hook_free);
free(head);
return ret;
}
@@ -91,8 +123,11 @@ static int run(int argc, const char **argv, const char *prefix,
int i;
struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
int ignore_missing = 0;
+ int allow_unknown = 0;
const char *hook_name;
struct option run_options[] = {
+ OPT_BOOL(0, "allow-unknown-hook-name", &allow_unknown,
+ N_("allow running a hook with a non-native hook name")),
OPT_BOOL(0, "ignore-missing", &ignore_missing,
N_("silently ignore missing requested <hook-name>")),
OPT_STRING(0, "to-stdin", &opt.path_to_stdin, N_("path"),
@@ -124,6 +159,14 @@ static int run(int argc, const char **argv, const char *prefix,
repo_config(the_repository, git_default_config, NULL);
hook_name = argv[0];
+
+ if (!allow_unknown && !is_known_hook(hook_name)) {
+ error(_("unknown hook event '%s';\n"
+ "use --allow-unknown-hook-name to allow non-native hook names"),
+ hook_name);
+ return 1;
+ }
+
if (!ignore_missing)
opt.error_if_missing = 1;
ret = run_hooks_opt(the_repository, hook_name, &opt);
diff --git a/builtin/log.c b/builtin/log.c
index 89e8b8f011..8c0939dd42 100644
--- a/builtin/log.c
+++ b/builtin/log.c
@@ -40,6 +40,7 @@
#include "progress.h"
#include "commit-slab.h"
#include "advice.h"
+#include "utf8.h"
#include "commit-reach.h"
#include "range-diff.h"
@@ -886,6 +887,7 @@ struct format_config {
char *signature;
char *signature_file;
enum cover_setting config_cover_letter;
+ char *fmt_cover_letter_commit_list;
char *config_output_directory;
enum cover_from_description cover_from_description_mode;
int show_notes;
@@ -930,6 +932,7 @@ static void format_config_release(struct format_config *cfg)
string_list_clear(&cfg->extra_cc, 0);
strbuf_release(&cfg->sprefix);
free(cfg->fmt_patch_suffix);
+ free(cfg->fmt_cover_letter_commit_list);
}
static enum cover_from_description parse_cover_from_description(const char *arg)
@@ -1052,6 +1055,10 @@ static int git_format_config(const char *var, const char *value,
cfg->config_cover_letter = git_config_bool(var, value) ? COVER_ON : COVER_OFF;
return 0;
}
+ if (!strcmp(var, "format.commitlistformat")) {
+ FREE_AND_NULL(cfg->fmt_cover_letter_commit_list);
+ return git_config_string(&cfg->fmt_cover_letter_commit_list, var, value);
+ }
if (!strcmp(var, "format.outputdirectory")) {
FREE_AND_NULL(cfg->config_output_directory);
return git_config_string(&cfg->config_output_directory, var, value);
@@ -1335,13 +1342,54 @@ static void get_notes_args(struct strvec *arg, struct rev_info *rev)
}
}
+static void generate_shortlog_cover_letter(struct shortlog *log,
+ struct rev_info *rev,
+ struct commit **list,
+ int nr)
+{
+ shortlog_init(log);
+ log->wrap_lines = 1;
+ log->wrap = MAIL_DEFAULT_WRAP;
+ log->in1 = 2;
+ log->in2 = 4;
+ log->file = rev->diffopt.file;
+ log->groups = SHORTLOG_GROUP_AUTHOR;
+ shortlog_finish_setup(log);
+ for (int i = 0; i < nr; i++)
+ shortlog_add_commit(log, list[i]);
+
+ shortlog_output(log);
+}
+
+static void generate_commit_list_cover(FILE *cover_file, const char *format,
+ struct commit **list, int n)
+{
+ struct strbuf commit_line = STRBUF_INIT;
+ struct pretty_print_context ctx = {0};
+ struct rev_info rev = REV_INFO_INIT;
+
+ rev.total = n;
+ ctx.rev = &rev;
+ for (int i = 1; i <= n; i++) {
+ rev.nr = i;
+ repo_format_commit_message(the_repository, list[n - i], format,
+ &commit_line, &ctx);
+ fprintf(cover_file, "%s\n", commit_line.buf);
+ strbuf_reset(&commit_line);
+ }
+ fprintf(cover_file, "\n");
+
+ strbuf_release(&commit_line);
+}
+
static void make_cover_letter(struct rev_info *rev, int use_separate_file,
struct commit *origin,
int nr, struct commit **list,
const char *description_file,
const char *branch_name,
int quiet,
- const struct format_config *cfg)
+ const struct format_config *cfg,
+ const char *format)
{
const char *from;
struct shortlog log;
@@ -1388,18 +1436,17 @@ static void make_cover_letter(struct rev_info *rev, int use_separate_file,
free(pp.after_subject);
strbuf_release(&sb);
- shortlog_init(&log);
- log.wrap_lines = 1;
- log.wrap = MAIL_DEFAULT_WRAP;
- log.in1 = 2;
- log.in2 = 4;
- log.file = rev->diffopt.file;
- log.groups = SHORTLOG_GROUP_AUTHOR;
- shortlog_finish_setup(&log);
- for (i = 0; i < nr; i++)
- shortlog_add_commit(&log, list[i]);
-
- shortlog_output(&log);
+ if (skip_prefix(format, "log:", &format))
+ generate_commit_list_cover(rev->diffopt.file, format, list, nr);
+ else if (!strcmp(format, "shortlog"))
+ generate_shortlog_cover_letter(&log, rev, list, nr);
+ else if (!strcmp(format, "modern"))
+ generate_commit_list_cover(rev->diffopt.file, "%w(72)[%(count)/%(total)] %s",
+ list, nr);
+ else if (strchr(format, '%'))
+ generate_commit_list_cover(rev->diffopt.file, format, list, nr);
+ else
+ die(_("'%s' is not a valid format string"), format);
/* We can only do diffstat with a unique reference point */
if (origin)
@@ -1917,6 +1964,7 @@ int cmd_format_patch(int argc,
int just_numbers = 0;
int ignore_if_in_upstream = 0;
int cover_letter = -1;
+ const char *cover_letter_fmt = NULL;
int boundary_count = 0;
int no_binary_diff = 0;
int zero_commit = 0;
@@ -1963,6 +2011,8 @@ int cmd_format_patch(int argc,
N_("print patches to standard out")),
OPT_BOOL(0, "cover-letter", &cover_letter,
N_("generate a cover letter")),
+ OPT_STRING(0, "commit-list-format", &cover_letter_fmt, N_("format-spec"),
+ N_("format spec used for the commit list in the cover letter")),
OPT_BOOL(0, "numbered-files", &just_numbers,
N_("use simple number sequence for output file names")),
OPT_STRING(0, "suffix", &fmt_patch_suffix, N_("sfx"),
@@ -2300,6 +2350,15 @@ int cmd_format_patch(int argc,
/* nothing to do */
goto done;
total = list.nr;
+
+ if (!cover_letter_fmt) {
+ cover_letter_fmt = cfg.fmt_cover_letter_commit_list;
+ if (!cover_letter_fmt)
+ cover_letter_fmt = "shortlog";
+ } else if (cover_letter == -1) {
+ cover_letter = 1;
+ }
+
if (cover_letter == -1) {
if (cfg.config_cover_letter == COVER_AUTO)
cover_letter = (total > 1);
@@ -2386,12 +2445,14 @@ int cmd_format_patch(int argc,
}
rev.numbered_files = just_numbers;
rev.patch_suffix = fmt_patch_suffix;
+
if (cover_letter) {
if (cfg.thread)
gen_message_id(&rev, "cover");
make_cover_letter(&rev, !!output_directory,
origin, list.nr, list.items,
- description_file, branch_name, quiet, &cfg);
+ description_file, branch_name, quiet, &cfg,
+ cover_letter_fmt);
print_bases(&bases, rev.diffopt.file);
print_signature(signature, rev.diffopt.file);
total++;
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index e34edff406..cb3656a034 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -3,46 +3,45 @@
#include "builtin.h"
#include "abspath.h"
-
+#include "commit.h"
+#include "commit-reach.h"
#include "config.h"
+#include "connect.h"
+#include "connected.h"
#include "environment.h"
+#include "exec-cmd.h"
+#include "fsck.h"
#include "gettext.h"
+#include "gpg-interface.h"
#include "hex.h"
-#include "lockfile.h"
-#include "pack.h"
-#include "refs.h"
-#include "pkt-line.h"
-#include "sideband.h"
-#include "run-command.h"
#include "hook.h"
-#include "exec-cmd.h"
-#include "commit.h"
+#include "lockfile.h"
#include "object.h"
-#include "remote.h"
-#include "connect.h"
-#include "string-list.h"
-#include "oid-array.h"
-#include "connected.h"
-#include "strvec.h"
-#include "version.h"
-#include "gpg-interface.h"
-#include "sigchain.h"
-#include "fsck.h"
-#include "tmp-objdir.h"
-#include "oidset.h"
-#include "packfile.h"
#include "object-file.h"
#include "object-name.h"
#include "odb.h"
+#include "oid-array.h"
+#include "oidset.h"
+#include "pack.h"
+#include "packfile.h"
+#include "parse-options.h"
+#include "pkt-line.h"
#include "protocol.h"
-#include "commit-reach.h"
+#include "refs.h"
+#include "remote.h"
+#include "run-command.h"
#include "server-info.h"
+#include "setup.h"
+#include "shallow.h"
+#include "sideband.h"
+#include "sigchain.h"
+#include "string-list.h"
+#include "strvec.h"
+#include "tmp-objdir.h"
#include "trace.h"
#include "trace2.h"
+#include "version.h"
#include "worktree.h"
-#include "shallow.h"
-#include "setup.h"
-#include "parse-options.h"
static const char * const receive_pack_usage[] = {
N_("git receive-pack <git-dir>"),
@@ -904,11 +903,14 @@ static int feed_receive_hook_cb(int hook_stdin_fd, void *pp_cb UNUSED, void *pp_
static void *receive_hook_feed_state_alloc(void *feed_pipe_ctx)
{
struct receive_hook_feed_state *init_state = feed_pipe_ctx;
- struct receive_hook_feed_state *data = xcalloc(1, sizeof(*data));
+ struct receive_hook_feed_state *data;
+
+ CALLOC_ARRAY(data, 1);
data->report = init_state->report;
data->cmd = init_state->cmd;
data->skip_broken = init_state->skip_broken;
strbuf_init(&data->buf, 0);
+
return data;
}
@@ -928,7 +930,11 @@ static int run_receive_hook(struct command *commands,
{
struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
struct command *iter = commands;
- struct receive_hook_feed_state feed_init_state = { 0 };
+ struct receive_hook_feed_state feed_init_state = {
+ .cmd = commands,
+ .skip_broken = skip_broken,
+ .buf = STRBUF_INIT,
+ };
struct async sideband_async;
int sideband_async_started = 0;
int saved_stderr = -1;
@@ -961,8 +967,6 @@ static int run_receive_hook(struct command *commands,
prepare_sideband_async(&sideband_async, &saved_stderr, &sideband_async_started);
/* set up stdin callback */
- feed_init_state.cmd = commands;
- feed_init_state.skip_broken = skip_broken;
opt.feed_pipe_ctx = &feed_init_state;
opt.feed_pipe = feed_receive_hook_cb;
opt.feed_pipe_cb_data_alloc = receive_hook_feed_state_alloc;
diff --git a/builtin/replay.c b/builtin/replay.c
index 2cdde830a8..a0879b020f 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -79,11 +79,12 @@ int cmd_replay(int argc,
struct ref_transaction *transaction = NULL;
struct strbuf transaction_err = STRBUF_INIT;
struct strbuf reflog_msg = STRBUF_INIT;
+ int desired_reverse;
int ret = 0;
const char *const replay_usage[] = {
N_("(EXPERIMENTAL!) git replay "
- "([--contained] --onto <newbase> | --advance <branch>) "
+ "([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) "
"[--ref-action[=<mode>]] <revision-range>"),
NULL
};
@@ -96,6 +97,9 @@ int cmd_replay(int argc,
N_("replay onto given commit")),
OPT_BOOL(0, "contained", &opts.contained,
N_("update all branches that point at commits in <revision-range>")),
+ OPT_STRING(0, "revert", &opts.revert,
+ N_("branch"),
+ N_("revert commits onto given branch")),
OPT_STRING(0, "ref-action", &ref_action,
N_("mode"),
N_("control ref update behavior (update|print)")),
@@ -105,19 +109,31 @@ int cmd_replay(int argc,
argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
- if (!opts.onto && !opts.advance) {
- error(_("option --onto or --advance is mandatory"));
+ /* Exactly one mode must be specified */
+ if (!opts.onto && !opts.advance && !opts.revert) {
+ error(_("exactly one of --onto, --advance, or --revert is required"));
usage_with_options(replay_usage, replay_options);
}
+ die_for_incompatible_opt3(!!opts.onto, "--onto",
+ !!opts.advance, "--advance",
+ !!opts.revert, "--revert");
die_for_incompatible_opt2(!!opts.advance, "--advance",
opts.contained, "--contained");
- die_for_incompatible_opt2(!!opts.advance, "--advance",
- !!opts.onto, "--onto");
+ die_for_incompatible_opt2(!!opts.revert, "--revert",
+ opts.contained, "--contained");
/* Parse ref action mode from command line or config */
ref_mode = get_ref_action_mode(repo, ref_action);
+ /*
+ * Cherry-pick/rebase need oldest-first ordering so that each
+ * replayed commit can build on its already-replayed parent.
+ * Revert needs newest-first ordering (like git revert) to
+ * reduce conflicts by peeling off changes from the top.
+ */
+ desired_reverse = !opts.revert;
+
repo_init_revisions(repo, &revs, prefix);
/*
@@ -129,7 +145,7 @@ int cmd_replay(int argc,
* some options changing these values if we think they could
* be useful.
*/
- revs.reverse = 1;
+ revs.reverse = desired_reverse;
revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
revs.topo_order = 1;
revs.simplify_history = 0;
@@ -144,11 +160,11 @@ int cmd_replay(int argc,
* Detect and warn if we override some user specified rev
* walking options.
*/
- if (revs.reverse != 1) {
+ if (revs.reverse != desired_reverse) {
warning(_("some rev walking options will be overridden as "
"'%s' bit in 'struct rev_info' will be forced"),
"reverse");
- revs.reverse = 1;
+ revs.reverse = desired_reverse;
}
if (revs.sort_order != REV_SORT_IN_GRAPH_ORDER) {
warning(_("some rev walking options will be overridden as "
@@ -174,7 +190,9 @@ int cmd_replay(int argc,
goto cleanup;
/* Build reflog message */
- if (opts.advance) {
+ if (opts.revert) {
+ strbuf_addf(&reflog_msg, "replay --revert %s", opts.revert);
+ } else if (opts.advance) {
strbuf_addf(&reflog_msg, "replay --advance %s", opts.advance);
} else {
struct object_id oid;
diff --git a/builtin/worktree.c b/builtin/worktree.c
index 4035b1cb06..4fd6f7575f 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -692,25 +692,8 @@ static int can_use_local_refs(const struct add_opts *opts)
if (refs_head_ref(get_main_ref_store(the_repository), first_valid_ref, NULL)) {
return 1;
} else if (refs_for_each_branch_ref(get_main_ref_store(the_repository), first_valid_ref, NULL)) {
- if (!opts->quiet) {
- struct strbuf path = STRBUF_INIT;
- struct strbuf contents = STRBUF_INIT;
- char *wt_gitdir = get_worktree_git_dir(NULL);
-
- strbuf_add_real_path(&path, wt_gitdir);
- strbuf_addstr(&path, "/HEAD");
- strbuf_read_file(&contents, path.buf, 64);
- strbuf_stripspace(&contents, NULL);
- strbuf_strip_suffix(&contents, "\n");
-
- warning(_("HEAD points to an invalid (or orphaned) reference.\n"
- "HEAD path: '%s'\n"
- "HEAD contents: '%s'"),
- path.buf, contents.buf);
- strbuf_release(&path);
- strbuf_release(&contents);
- free(wt_gitdir);
- }
+ if (!opts->quiet)
+ warning(_("HEAD points to an invalid (or orphaned) reference.\n"));
return 1;
}
return 0;
diff --git a/hook.c b/hook.c
index 2c8252b2c4..cc23276d27 100644
--- a/hook.c
+++ b/hook.c
@@ -1,16 +1,16 @@
#include "git-compat-util.h"
#include "abspath.h"
#include "advice.h"
+#include "config.h"
+#include "environment.h"
#include "gettext.h"
#include "hook.h"
-#include "path.h"
#include "parse.h"
+#include "path.h"
#include "run-command.h"
-#include "config.h"
+#include "setup.h"
#include "strbuf.h"
#include "strmap.h"
-#include "environment.h"
-#include "setup.h"
const char *find_hook(struct repository *r, const char *name)
{
@@ -52,34 +52,26 @@ const char *find_hook(struct repository *r, const char *name)
return path.buf;
}
-static void hook_clear(struct hook *h, cb_data_free_fn cb_data_free)
+void hook_free(void *p, const char *str UNUSED)
{
+ struct hook *h = p;
+
if (!h)
return;
- if (h->kind == HOOK_TRADITIONAL)
+ if (h->kind == HOOK_TRADITIONAL) {
free((void *)h->u.traditional.path);
- else if (h->kind == HOOK_CONFIGURED) {
+ } else if (h->kind == HOOK_CONFIGURED) {
free((void *)h->u.configured.friendly_name);
free((void *)h->u.configured.command);
}
- if (cb_data_free)
- cb_data_free(h->feed_pipe_cb_data);
+ if (h->data_free && h->feed_pipe_cb_data)
+ h->data_free(h->feed_pipe_cb_data);
free(h);
}
-void hook_list_clear(struct string_list *hooks, cb_data_free_fn cb_data_free)
-{
- struct string_list_item *item;
-
- for_each_string_list_item(item, hooks)
- hook_clear(item->util, cb_data_free);
-
- string_list_clear(hooks, 0);
-}
-
/* Helper to detect and add default "traditional" hooks from the hookdir. */
static void list_hooks_add_default(struct repository *r, const char *hookname,
struct string_list *hook_list,
@@ -91,7 +83,7 @@ static void list_hooks_add_default(struct repository *r, const char *hookname,
if (!hook_path)
return;
- h = xcalloc(1, sizeof(struct hook));
+ CALLOC_ARRAY(h, 1);
/*
* If the hook is to run in a specific dir, a relative path can
@@ -100,9 +92,15 @@ static void list_hooks_add_default(struct repository *r, const char *hookname,
if (options && options->dir)
hook_path = absolute_path(hook_path);
- /* Setup per-hook internal state cb data */
- if (options && options->feed_pipe_cb_data_alloc)
+ /*
+ * Setup per-hook internal state callback data.
+ * When provided, the alloc/free callbacks are always provided
+ * together, so use them to alloc/free the internal hook state.
+ */
+ if (options && options->feed_pipe_cb_data_alloc) {
h->feed_pipe_cb_data = options->feed_pipe_cb_data_alloc(options->feed_pipe_ctx);
+ h->data_free = options->feed_pipe_cb_data_free;
+ }
h->kind = HOOK_TRADITIONAL;
h->u.traditional.path = xstrdup(hook_path);
@@ -110,19 +108,21 @@ static void list_hooks_add_default(struct repository *r, const char *hookname,
string_list_append(hook_list, hook_path)->util = h;
}
-static void unsorted_string_list_remove(struct string_list *list,
- const char *str)
-{
- struct string_list_item *item = unsorted_string_list_lookup(list, str);
- if (item)
- unsorted_string_list_delete_item(list, item - list->items, 0);
-}
+/*
+ * Cache entry stored as the .util pointer of string_list items inside the
+ * hook config cache.
+ */
+struct hook_config_cache_entry {
+ char *command;
+ enum config_scope scope;
+ bool disabled;
+};
/*
* Callback struct to collect all hook.* keys in a single config pass.
* commands: friendly-name to command map.
* event_hooks: event-name to list of friendly-names map.
- * disabled_hooks: set of friendly-names with hook.name.enabled = false.
+ * disabled_hooks: set of friendly-names with hook.<friendly-name>.enabled = false.
*/
struct hook_all_config_cb {
struct strmap commands;
@@ -132,7 +132,7 @@ struct hook_all_config_cb {
/* repo_config() callback that collects all hook.* configuration in one pass. */
static int hook_config_lookup_all(const char *key, const char *value,
- const struct config_context *ctx UNUSED,
+ const struct config_context *ctx,
void *cb_data)
{
struct hook_all_config_cb *data = cb_data;
@@ -156,20 +156,32 @@ static int hook_config_lookup_all(const char *key, const char *value,
struct strmap_entry *e;
strmap_for_each_entry(&data->event_hooks, &iter, e)
- unsorted_string_list_remove(e->value, hook_name);
+ unsorted_string_list_remove(e->value, hook_name, 0);
} else {
struct string_list *hooks =
strmap_get(&data->event_hooks, value);
if (!hooks) {
- hooks = xcalloc(1, sizeof(*hooks));
+ CALLOC_ARRAY(hooks, 1);
string_list_init_dup(hooks);
strmap_put(&data->event_hooks, value, hooks);
}
/* Re-insert if necessary to preserve last-seen order. */
- unsorted_string_list_remove(hooks, hook_name);
- string_list_append(hooks, hook_name);
+ unsorted_string_list_remove(hooks, hook_name, 0);
+
+ if (!ctx->kvi)
+ BUG("hook config callback called without key-value info");
+
+ /*
+ * Stash the config scope in the util pointer for
+ * later retrieval in build_hook_config_map(). This
+ * intermediate struct is transient and never leaves
+ * that function, so we pack the enum value into the
+ * pointer rather than heap-allocating a wrapper.
+ */
+ string_list_append(hooks, hook_name)->util =
+ (void *)(uintptr_t)ctx->kvi->scope;
}
} else if (!strcmp(subkey, "command")) {
/* Store command overwriting the old value */
@@ -186,7 +198,7 @@ static int hook_config_lookup_all(const char *key, const char *value,
break;
case 1: /* enabled: undo a prior disabled entry */
unsorted_string_list_remove(&data->disabled_hooks,
- hook_name);
+ hook_name, 0);
break;
default:
break; /* ignore unrecognised values */
@@ -202,8 +214,10 @@ static int hook_config_lookup_all(const char *key, const char *value,
* every item's string is the hook's friendly-name and its util pointer is
* the corresponding command string. Both strings are owned by the map.
*
- * Disabled hooks and hooks missing a command are already filtered out at
- * parse time, so callers can iterate the list directly.
+ * Disabled hooks are kept in the cache with entry->disabled set, so that
+ * "git hook list" can display them. A non-disabled hook missing a command
+ * is fatal; a disabled hook missing a command emits a warning and is kept
+ * in the cache with entry->command = NULL.
*/
void hook_cache_clear(struct strmap *cache)
{
@@ -212,7 +226,12 @@ void hook_cache_clear(struct strmap *cache)
strmap_for_each_entry(cache, &iter, e) {
struct string_list *hooks = e->value;
- string_list_clear(hooks, 1); /* free util (command) pointers */
+ for (size_t i = 0; i < hooks->nr; i++) {
+ struct hook_config_cache_entry *entry = hooks->items[i].util;
+ free(entry->command);
+ free(entry);
+ }
+ string_list_clear(hooks, 0);
free(hooks);
}
strmap_clear(cache, 0);
@@ -235,28 +254,39 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache)
/* Construct the cache from parsed configs. */
strmap_for_each_entry(&cb_data.event_hooks, &iter, e) {
struct string_list *hook_names = e->value;
- struct string_list *hooks = xcalloc(1, sizeof(*hooks));
+ struct string_list *hooks;
+ CALLOC_ARRAY(hooks, 1);
string_list_init_dup(hooks);
for (size_t i = 0; i < hook_names->nr; i++) {
const char *hname = hook_names->items[i].string;
+ enum config_scope scope =
+ (enum config_scope)(uintptr_t)hook_names->items[i].util;
+ struct hook_config_cache_entry *entry;
char *command;
- /* filter out disabled hooks */
- if (unsorted_string_list_lookup(&cb_data.disabled_hooks,
- hname))
- continue;
+ bool is_disabled =
+ !!unsorted_string_list_lookup(
+ &cb_data.disabled_hooks, hname);
command = strmap_get(&cb_data.commands, hname);
- if (!command)
- die(_("'hook.%s.command' must be configured or "
- "'hook.%s.event' must be removed;"
- " aborting."), hname, hname);
+ if (!command) {
+ if (is_disabled)
+ warning(_("disabled hook '%s' has no "
+ "command configured"), hname);
+ else
+ die(_("'hook.%s.command' must be configured or "
+ "'hook.%s.event' must be removed;"
+ " aborting."), hname, hname);
+ }
- /* util stores the command; owned by the cache. */
- string_list_append(hooks, hname)->util =
- xstrdup(command);
+ /* util stores a cache entry; owned by the cache. */
+ CALLOC_ARRAY(entry, 1);
+ entry->command = xstrdup_or_null(command);
+ entry->scope = scope;
+ entry->disabled = is_disabled;
+ string_list_append(hooks, hname)->util = entry;
}
strmap_put(cache, e->key, hooks);
@@ -289,7 +319,7 @@ static struct strmap *get_hook_config_cache(struct repository *r)
* it just once on the first call.
*/
if (!r->hook_config_cache) {
- r->hook_config_cache = xcalloc(1, sizeof(*cache));
+ CALLOC_ARRAY(r->hook_config_cache, 1);
strmap_init(r->hook_config_cache);
build_hook_config_map(r, r->hook_config_cache);
}
@@ -297,9 +327,9 @@ static struct strmap *get_hook_config_cache(struct repository *r)
} else {
/*
* Out-of-repo calls (no gitdir) allocate and return a temporary
- * map cache which gets free'd immediately by the caller.
+ * cache which gets freed immediately by the caller.
*/
- cache = xcalloc(1, sizeof(*cache));
+ CALLOC_ARRAY(cache, 1);
strmap_init(cache);
build_hook_config_map(r, cache);
}
@@ -318,17 +348,28 @@ static void list_hooks_add_configured(struct repository *r,
/* Iterate through configured hooks and initialize internal states */
for (size_t i = 0; configured_hooks && i < configured_hooks->nr; i++) {
const char *friendly_name = configured_hooks->items[i].string;
- const char *command = configured_hooks->items[i].util;
- struct hook *hook = xcalloc(1, sizeof(struct hook));
+ struct hook_config_cache_entry *entry = configured_hooks->items[i].util;
+ struct hook *hook;
- if (options && options->feed_pipe_cb_data_alloc)
+ CALLOC_ARRAY(hook, 1);
+
+ /*
+ * When provided, the alloc/free callbacks are always provided
+ * together, so use them to alloc/free the internal hook state.
+ */
+ if (options && options->feed_pipe_cb_data_alloc) {
hook->feed_pipe_cb_data =
options->feed_pipe_cb_data_alloc(
options->feed_pipe_ctx);
+ hook->data_free = options->feed_pipe_cb_data_free;
+ }
hook->kind = HOOK_CONFIGURED;
hook->u.configured.friendly_name = xstrdup(friendly_name);
- hook->u.configured.command = xstrdup(command);
+ hook->u.configured.command =
+ entry->command ? xstrdup(entry->command) : NULL;
+ hook->u.configured.scope = entry->scope;
+ hook->u.configured.disabled = entry->disabled;
string_list_append(list, friendly_name)->util = hook;
}
@@ -351,7 +392,7 @@ struct string_list *list_hooks(struct repository *r, const char *hookname,
if (!hookname)
BUG("null hookname was provided to hook_list()!");
- hook_head = xmalloc(sizeof(struct string_list));
+ CALLOC_ARRAY(hook_head, 1);
string_list_init_dup(hook_head);
/* Add hooks from the config, e.g. hook.myhook.event = pre-commit */
@@ -366,8 +407,17 @@ struct string_list *list_hooks(struct repository *r, const char *hookname,
int hook_exists(struct repository *r, const char *name)
{
struct string_list *hooks = list_hooks(r, name, NULL);
- int exists = hooks->nr > 0;
- hook_list_clear(hooks, NULL);
+ int exists = 0;
+
+ for (size_t i = 0; i < hooks->nr; i++) {
+ struct hook *h = hooks->items[i].util;
+ if (h->kind == HOOK_TRADITIONAL ||
+ !h->u.configured.disabled) {
+ exists = 1;
+ break;
+ }
+ }
+ string_list_clear_func(hooks, hook_free);
free(hooks);
return exists;
}
@@ -381,10 +431,11 @@ static int pick_next_hook(struct child_process *cp,
struct string_list *hook_list = hook_cb->hook_command_list;
struct hook *h;
- if (hook_cb->hook_to_run_index >= hook_list->nr)
- return 0;
-
- h = hook_list->items[hook_cb->hook_to_run_index++].util;
+ do {
+ if (hook_cb->hook_to_run_index >= hook_list->nr)
+ return 0;
+ h = hook_list->items[hook_cb->hook_to_run_index++].util;
+ } while (h->kind == HOOK_CONFIGURED && h->u.configured.disabled);
cp->no_stdin = 1;
strvec_pushv(&cp->env, hook_cb->options->env.v);
@@ -414,7 +465,11 @@ static int pick_next_hook(struct child_process *cp,
} else if (h->kind == HOOK_CONFIGURED) {
/* to enable oneliners, let config-specified hooks run in shell. */
cp->use_shell = true;
+ if (!h->u.configured.command)
+ BUG("non-disabled HOOK_CONFIGURED hook has no command");
strvec_push(&cp->args, h->u.configured.command);
+ } else {
+ BUG("unknown hook kind");
}
if (!cp->args.nr)
@@ -501,8 +556,7 @@ int run_hooks_opt(struct repository *r, const char *hook_name,
* Ensure cb_data copy and free functions are either provided together,
* or neither one is provided.
*/
- if ((options->feed_pipe_cb_data_alloc && !options->feed_pipe_cb_data_free) ||
- (!options->feed_pipe_cb_data_alloc && options->feed_pipe_cb_data_free))
+ if (!options->feed_pipe_cb_data_alloc != !options->feed_pipe_cb_data_free)
BUG("feed_pipe_cb_data_alloc and feed_pipe_cb_data_free must be set together");
if (options->invoked_hook)
@@ -518,7 +572,7 @@ int run_hooks_opt(struct repository *r, const char *hook_name,
run_processes_parallel(&opts);
ret = cb_data.rc;
cleanup:
- hook_list_clear(cb_data.hook_command_list, options->feed_pipe_cb_data_free);
+ string_list_clear_func(cb_data.hook_command_list, hook_free);
free(cb_data.hook_command_list);
run_hooks_opt_clear(options);
return ret;
diff --git a/hook.h b/hook.h
index e949f5d488..5c5628dd1f 100644
--- a/hook.h
+++ b/hook.h
@@ -1,17 +1,21 @@
#ifndef HOOK_H
#define HOOK_H
-#include "strvec.h"
+#include "config.h"
#include "run-command.h"
#include "string-list.h"
#include "strmap.h"
+#include "strvec.h"
struct repository;
+typedef void (*hook_data_free_fn)(void *data);
+typedef void *(*hook_data_alloc_fn)(void *init_ctx);
+
/**
* Represents a hook command to be run.
* Hooks can be:
* 1. "traditional" (found in the hooks directory)
- * 2. "configured" (defined in Git's configuration via hook.<name>.event).
+ * 2. "configured" (defined in Git's configuration via hook.<friendly-name>.event).
* The 'kind' field determines which part of the union 'u' is valid.
*/
struct hook {
@@ -26,6 +30,8 @@ struct hook {
struct {
const char *friendly_name;
const char *command;
+ enum config_scope scope;
+ bool disabled;
} configured;
} u;
@@ -41,13 +47,17 @@ struct hook {
* Only useful when using `run_hooks_opt.feed_pipe`, otherwise ignore it.
*/
void *feed_pipe_cb_data;
-};
-typedef void (*cb_data_free_fn)(void *data);
-typedef void *(*cb_data_alloc_fn)(void *init_ctx);
+ /**
+ * Callback to free `feed_pipe_cb_data`.
+ *
+ * It is called automatically and points to the `feed_pipe_cb_data_free`
+ * provided via the `run_hook_opt` parameter.
+ */
+ hook_data_free_fn data_free;
+};
-struct run_hooks_opt
-{
+struct run_hooks_opt {
/* Environment vars to be set for each hook */
struct strvec env;
@@ -132,14 +142,14 @@ struct run_hooks_opt
*
* The `feed_pipe_ctx` pointer can be used to pass initialization data.
*/
- cb_data_alloc_fn feed_pipe_cb_data_alloc;
+ hook_data_alloc_fn feed_pipe_cb_data_alloc;
/**
* Called to free the memory initialized by `feed_pipe_cb_data_alloc`.
*
* Must always be provided when `feed_pipe_cb_data_alloc` is provided.
*/
- cb_data_free_fn feed_pipe_cb_data_free;
+ hook_data_free_fn feed_pipe_cb_data_free;
};
#define RUN_HOOKS_OPT_INIT { \
@@ -186,10 +196,10 @@ struct string_list *list_hooks(struct repository *r, const char *hookname,
struct run_hooks_opt *options);
/**
- * Frees the memory allocated for the hook list, including the `struct hook`
- * items and their internal state.
+ * Frees a struct hook stored as the util pointer of a string_list_item.
+ * Suitable for use as a string_list_clear_func_t callback.
*/
-void hook_list_clear(struct string_list *hooks, cb_data_free_fn cb_data_free);
+void hook_free(void *p, const char *str);
/**
* Frees the hook configuration cache stored in `struct repository`.
diff --git a/path-walk.c b/path-walk.c
index 364e4cfa19..2aa3e7d8a4 100644
--- a/path-walk.c
+++ b/path-walk.c
@@ -11,6 +11,7 @@
#include "list-objects.h"
#include "object.h"
#include "oid-array.h"
+#include "path.h"
#include "prio-queue.h"
#include "repository.h"
#include "revision.h"
@@ -62,6 +63,8 @@ struct path_walk_context {
*/
struct prio_queue path_stack;
struct strset path_stack_pushed;
+
+ unsigned exact_pathspecs:1;
};
static int compare_by_type(const void *one, const void *two, void *cb_data)
@@ -206,6 +209,33 @@ static int add_tree_entries(struct path_walk_context *ctx,
match != MATCHED)
continue;
}
+ if (ctx->revs->prune_data.nr && ctx->exact_pathspecs) {
+ struct pathspec *pd = &ctx->revs->prune_data;
+ bool found = false;
+ int did_strip_suffix = strbuf_strip_suffix(&path, "/");
+
+
+ for (int i = 0; i < pd->nr; i++) {
+ struct pathspec_item *item = &pd->items[i];
+
+ /*
+ * Continue if either is a directory prefix
+ * of the other.
+ */
+ if (dir_prefix(path.buf, item->match) ||
+ dir_prefix(item->match, path.buf)) {
+ found = true;
+ break;
+ }
+ }
+
+ if (did_strip_suffix)
+ strbuf_addch(&path, '/');
+
+ /* Skip paths that do not match the prefix. */
+ if (!found)
+ continue;
+ }
add_path_to_list(ctx, path.buf, type, &entry.oid,
!(o->flags & UNINTERESTING));
@@ -274,6 +304,13 @@ static int walk_path(struct path_walk_context *ctx,
return 0;
}
+ if (list->type == OBJ_BLOB &&
+ ctx->revs->prune_data.nr &&
+ !match_pathspec(ctx->repo->index, &ctx->revs->prune_data,
+ path, strlen(path), 0,
+ NULL, 0))
+ return 0;
+
/* Evaluate function pointer on this data, if requested. */
if ((list->type == OBJ_TREE && ctx->info->trees) ||
(list->type == OBJ_BLOB && ctx->info->blobs) ||
@@ -481,6 +518,12 @@ int walk_objects_by_path(struct path_walk_info *info)
if (info->tags)
info->revs->tag_objects = 1;
+ if (ctx.revs->prune_data.nr) {
+ if (!ctx.revs->prune_data.has_wildcard &&
+ !ctx.revs->prune_data.magic)
+ ctx.exact_pathspecs = 1;
+ }
+
/* Insert a single list for the root tree into the paths. */
CALLOC_ARRAY(root_tree_list, 1);
root_tree_list->type = OBJ_TREE;
diff --git a/path.c b/path.c
index c285357859..d7e17bf174 100644
--- a/path.c
+++ b/path.c
@@ -56,7 +56,7 @@ static void strbuf_cleanup_path(struct strbuf *sb)
strbuf_remove(sb, 0, path - sb->buf);
}
-static int dir_prefix(const char *buf, const char *dir)
+int dir_prefix(const char *buf, const char *dir)
{
size_t len = strlen(dir);
return !strncmp(buf, dir, len) &&
diff --git a/path.h b/path.h
index cbcad254a0..0434ba5e07 100644
--- a/path.h
+++ b/path.h
@@ -112,6 +112,12 @@ const char *repo_submodule_path_replace(struct repository *repo,
const char *fmt, ...)
__attribute__((format (printf, 4, 5)));
+/*
+ * Given a directory name 'dir' (not ending with a trailing '/'),
+ * determine if 'buf' is equal to 'dir' or has prefix 'dir'+'/'.
+ */
+int dir_prefix(const char *buf, const char *dir);
+
void report_linked_checkout_garbage(struct repository *r);
/*
diff --git a/pretty.c b/pretty.c
index ebf4da4834..814803980b 100644
--- a/pretty.c
+++ b/pretty.c
@@ -1549,6 +1549,21 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */
if (!commit->object.parsed)
parse_object(the_repository, &commit->object.oid);
+ if (starts_with(placeholder, "(count)")) {
+ if (!c->pretty_ctx->rev)
+ die(_("%s is not supported by this command"), "%(count)");
+ strbuf_addf(sb, "%0*d", decimal_width(c->pretty_ctx->rev->total),
+ c->pretty_ctx->rev->nr);
+ return 7;
+ }
+
+ if (starts_with(placeholder, "(total)")) {
+ if (!c->pretty_ctx->rev)
+ die(_("%s is not supported by this command"), "%(total)");
+ strbuf_addf(sb, "%d", c->pretty_ctx->rev->total);
+ return 7;
+ }
+
switch (placeholder[0]) {
case 'H': /* commit hash */
strbuf_addstr(sb, diff_get_color(c->auto_color, DIFF_COMMIT));
diff --git a/refs.c b/refs.c
index 685a0c247b..5d1d28523d 100644
--- a/refs.c
+++ b/refs.c
@@ -2595,7 +2595,8 @@ static int transaction_hook_feed_stdin(int hook_stdin_fd, void *pp_cb, void *pp_
static void *transaction_feed_cb_data_alloc(void *feed_pipe_ctx UNUSED)
{
- struct transaction_feed_cb_data *data = xmalloc(sizeof(*data));
+ struct transaction_feed_cb_data *data;
+ CALLOC_ARRAY(data, 1);
strbuf_init(&data->buf, 0);
data->index = 0;
return data;
diff --git a/replay.c b/replay.c
index a63f6714c4..d7239d4c83 100644
--- a/replay.c
+++ b/replay.c
@@ -8,6 +8,7 @@
#include "refs.h"
#include "replay.h"
#include "revision.h"
+#include "sequencer.h"
#include "strmap.h"
#include "tree.h"
@@ -17,6 +18,11 @@
*/
#define the_repository DO_NOT_USE_THE_REPOSITORY
+enum replay_mode {
+ REPLAY_MODE_PICK,
+ REPLAY_MODE_REVERT,
+};
+
static const char *short_commit_name(struct repository *repo,
struct commit *commit)
{
@@ -50,15 +56,37 @@ static char *get_author(const char *message)
return NULL;
}
+static void generate_revert_message(struct strbuf *msg,
+ struct commit *commit,
+ struct repository *repo)
+{
+ const char *out_enc = get_commit_output_encoding();
+ const char *message = repo_logmsg_reencode(repo, commit, NULL, out_enc);
+ const char *subject_start;
+ int subject_len;
+ char *subject;
+
+ subject_len = find_commit_subject(message, &subject_start);
+ subject = xmemdupz(subject_start, subject_len);
+
+ sequencer_format_revert_message(repo, subject, commit,
+ commit->parents ? commit->parents->item : NULL,
+ false, msg);
+
+ free(subject);
+ repo_unuse_commit_buffer(repo, commit, message);
+}
+
static struct commit *create_commit(struct repository *repo,
struct tree *tree,
struct commit *based_on,
- struct commit *parent)
+ struct commit *parent,
+ enum replay_mode mode)
{
struct object_id ret;
struct object *obj = NULL;
struct commit_list *parents = NULL;
- char *author;
+ char *author = NULL;
char *sign_commit = NULL; /* FIXME: cli users might want to sign again */
struct commit_extra_header *extra = NULL;
struct strbuf msg = STRBUF_INIT;
@@ -70,9 +98,16 @@ static struct commit *create_commit(struct repository *repo,
commit_list_insert(parent, &parents);
extra = read_commit_extra_headers(based_on, exclude_gpgsig);
- find_commit_subject(message, &orig_message);
- strbuf_addstr(&msg, orig_message);
- author = get_author(message);
+ if (mode == REPLAY_MODE_REVERT) {
+ generate_revert_message(&msg, based_on, repo);
+ /* For revert, use current user as author (NULL = use default) */
+ } else if (mode == REPLAY_MODE_PICK) {
+ find_commit_subject(message, &orig_message);
+ strbuf_addstr(&msg, orig_message);
+ author = get_author(message);
+ } else {
+ BUG("unexpected replay mode %d", mode);
+ }
reset_ident_date();
if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,
&ret, author, NULL, sign_commit, extra)) {
@@ -153,11 +188,35 @@ static void get_ref_information(struct repository *repo,
}
}
+static void set_up_branch_mode(struct repository *repo,
+ char **branch_name,
+ const char *option_name,
+ struct ref_info *rinfo,
+ struct commit **onto)
+{
+ struct object_id oid;
+ char *fullname = NULL;
+
+ if (repo_dwim_ref(repo, *branch_name, strlen(*branch_name),
+ &oid, &fullname, 0) == 1) {
+ free(*branch_name);
+ *branch_name = fullname;
+ } else {
+ die(_("argument to %s must be a reference"), option_name);
+ }
+ *onto = peel_committish(repo, *branch_name, option_name);
+ if (rinfo->positive_refexprs > 1)
+ die(_("'%s' cannot be used with multiple revision ranges "
+ "because the ordering would be ill-defined"),
+ option_name);
+}
+
static void set_up_replay_mode(struct repository *repo,
struct rev_cmdline_info *cmd_info,
const char *onto_name,
bool *detached_head,
char **advance_name,
+ char **revert_name,
struct commit **onto,
struct strset **update_refs)
{
@@ -172,9 +231,6 @@ static void set_up_replay_mode(struct repository *repo,
if (!rinfo.positive_refexprs)
die(_("need some commits to replay"));
- if (!onto_name == !*advance_name)
- BUG("one and only one of onto_name and *advance_name must be given");
-
if (onto_name) {
*onto = peel_committish(repo, onto_name, "--onto");
if (rinfo.positive_refexprs <
@@ -183,23 +239,12 @@ static void set_up_replay_mode(struct repository *repo,
*update_refs = xcalloc(1, sizeof(**update_refs));
**update_refs = rinfo.positive_refs;
memset(&rinfo.positive_refs, 0, sizeof(**update_refs));
+ } else if (*advance_name) {
+ set_up_branch_mode(repo, advance_name, "--advance", &rinfo, onto);
+ } else if (*revert_name) {
+ set_up_branch_mode(repo, revert_name, "--revert", &rinfo, onto);
} else {
- struct object_id oid;
- char *fullname = NULL;
-
- if (!*advance_name)
- BUG("expected either onto_name or *advance_name in this function");
-
- if (repo_dwim_ref(repo, *advance_name, strlen(*advance_name),
- &oid, &fullname, 0) == 1) {
- free(*advance_name);
- *advance_name = fullname;
- } else {
- die(_("argument to --advance must be a reference"));
- }
- *onto = peel_committish(repo, *advance_name, "--advance");
- if (rinfo.positive_refexprs > 1)
- die(_("cannot advance target with multiple sources because ordering would be ill-defined"));
+ BUG("expected one of onto_name, *advance_name, or *revert_name");
}
strset_clear(&rinfo.negative_refs);
strset_clear(&rinfo.positive_refs);
@@ -220,7 +265,8 @@ static struct commit *pick_regular_commit(struct repository *repo,
kh_oid_map_t *replayed_commits,
struct commit *onto,
struct merge_options *merge_opt,
- struct merge_result *result)
+ struct merge_result *result,
+ enum replay_mode mode)
{
struct commit *base, *replayed_base;
struct tree *pickme_tree, *base_tree, *replayed_base_tree;
@@ -232,25 +278,45 @@ static struct commit *pick_regular_commit(struct repository *repo,
pickme_tree = repo_get_commit_tree(repo, pickme);
base_tree = repo_get_commit_tree(repo, base);
- merge_opt->branch1 = short_commit_name(repo, replayed_base);
- merge_opt->branch2 = short_commit_name(repo, pickme);
- merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
+ if (mode == REPLAY_MODE_PICK) {
+ /* Cherry-pick: normal order */
+ merge_opt->branch1 = short_commit_name(repo, replayed_base);
+ merge_opt->branch2 = short_commit_name(repo, pickme);
+ merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
+
+ merge_incore_nonrecursive(merge_opt,
+ base_tree,
+ replayed_base_tree,
+ pickme_tree,
+ result);
+
+ free((char *)merge_opt->ancestor);
+ } else if (mode == REPLAY_MODE_REVERT) {
+ /* Revert: swap base and pickme to reverse the diff */
+ const char *pickme_name = short_commit_name(repo, pickme);
+ merge_opt->branch1 = short_commit_name(repo, replayed_base);
+ merge_opt->branch2 = xstrfmt("parent of %s", pickme_name);
+ merge_opt->ancestor = pickme_name;
- merge_incore_nonrecursive(merge_opt,
- base_tree,
- replayed_base_tree,
- pickme_tree,
- result);
+ merge_incore_nonrecursive(merge_opt,
+ pickme_tree,
+ replayed_base_tree,
+ base_tree,
+ result);
- free((char*)merge_opt->ancestor);
+ free((char *)merge_opt->branch2);
+ } else {
+ BUG("unexpected replay mode %d", mode);
+ }
merge_opt->ancestor = NULL;
+ merge_opt->branch2 = NULL;
if (!result->clean)
return NULL;
/* Drop commits that become empty */
if (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&
!oideq(&pickme_tree->object.oid, &base_tree->object.oid))
return replayed_base;
- return create_commit(repo, result->tree, pickme, replayed_base);
+ return create_commit(repo, result->tree, pickme, replayed_base, mode);
}
void replay_result_release(struct replay_result *result)
@@ -287,11 +353,16 @@ int replay_revisions(struct rev_info *revs,
};
bool detached_head;
char *advance;
+ char *revert;
+ enum replay_mode mode = REPLAY_MODE_PICK;
int ret;
advance = xstrdup_or_null(opts->advance);
+ revert = xstrdup_or_null(opts->revert);
+ if (revert)
+ mode = REPLAY_MODE_REVERT;
set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto,
- &detached_head, &advance, &onto, &update_refs);
+ &detached_head, &advance, &revert, &onto, &update_refs);
/* FIXME: Should allow replaying commits with the first as a root commit */
@@ -315,7 +386,8 @@ int replay_revisions(struct rev_info *revs,
die(_("replaying merge commits is not supported yet!"));
last_commit = pick_regular_commit(revs->repo, commit, replayed_commits,
- onto, &merge_opt, &result);
+ mode == REPLAY_MODE_REVERT ? last_commit : onto,
+ &merge_opt, &result, mode);
if (!last_commit)
break;
@@ -327,7 +399,7 @@ int replay_revisions(struct rev_info *revs,
kh_value(replayed_commits, pos) = last_commit;
/* Update any necessary branches */
- if (advance)
+ if (advance || revert)
continue;
for (decoration = get_name_decoration(&commit->object);
@@ -361,11 +433,13 @@ int replay_revisions(struct rev_info *revs,
goto out;
}
- /* In --advance mode, advance the target ref */
- if (advance)
- replay_result_queue_update(out, advance,
+ /* In --advance or --revert mode, update the target ref */
+ if (advance || revert) {
+ const char *ref = advance ? advance : revert;
+ replay_result_queue_update(out, ref,
&onto->object.oid,
&last_commit->object.oid);
+ }
ret = 0;
@@ -377,5 +451,6 @@ out:
kh_destroy_oid_map(replayed_commits);
merge_finalize(&merge_opt, &result);
free(advance);
+ free(revert);
return ret;
}
diff --git a/replay.h b/replay.h
index d8407dc7f7..e916a5f975 100644
--- a/replay.h
+++ b/replay.h
@@ -13,7 +13,7 @@ struct replay_revisions_options {
/*
* Starting point at which to create the new commits; must be a branch
* name. The branch will be updated to point to the rewritten commits.
- * This option is mutually exclusive with `onto`.
+ * This option is mutually exclusive with `onto` and `revert`.
*/
const char *advance;
@@ -22,7 +22,14 @@ struct replay_revisions_options {
* committish. References pointing at decendants of `onto` will be
* updated to point to the new commits.
*/
- const char *onto;
+ const char *onto;
+
+ /*
+ * Starting point at which to create revert commits; must be a branch
+ * name. The branch will be updated to point to the revert commits.
+ * This option is mutually exclusive with `onto` and `advance`.
+ */
+ const char *revert;
/*
* Update branches that point at commits in the given revision range.
diff --git a/revision.h b/revision.h
index 69242ecb18..584f1338b5 100644
--- a/revision.h
+++ b/revision.h
@@ -4,6 +4,7 @@
#include "commit.h"
#include "grep.h"
#include "notes.h"
+#include "object-name.h"
#include "oidset.h"
#include "pretty.h"
#include "diff.h"
diff --git a/sequencer.c b/sequencer.c
index e5af49cecd..b7d8dca47f 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -2211,15 +2211,16 @@ static int should_edit(struct replay_opts *opts) {
return opts->edit;
}
-static void refer_to_commit(struct replay_opts *opts,
- struct strbuf *msgbuf, struct commit *commit)
+static void refer_to_commit(struct repository *r, struct strbuf *msgbuf,
+ const struct commit *commit,
+ bool use_commit_reference)
{
- if (opts->commit_use_reference) {
+ if (use_commit_reference) {
struct pretty_print_context ctx = {
.abbrev = DEFAULT_ABBREV,
.date_mode.type = DATE_SHORT,
};
- repo_format_commit_message(the_repository, commit,
+ repo_format_commit_message(r, commit,
"%h (%s, %ad)", msgbuf, &ctx);
} else {
strbuf_addstr(msgbuf, oid_to_hex(&commit->object.oid));
@@ -2369,38 +2370,14 @@ static int do_pick_commit(struct repository *r,
*/
if (command == TODO_REVERT) {
- const char *orig_subject;
-
base = commit;
base_label = msg.label;
next = parent;
next_label = msg.parent_label;
- if (opts->commit_use_reference) {
- strbuf_commented_addf(&ctx->message, comment_line_str,
- "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***");
- } else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) &&
- /*
- * We don't touch pre-existing repeated reverts, because
- * theoretically these can be nested arbitrarily deeply,
- * thus requiring excessive complexity to deal with.
- */
- !starts_with(orig_subject, "Revert \"")) {
- strbuf_addstr(&ctx->message, "Reapply \"");
- strbuf_addstr(&ctx->message, orig_subject);
- strbuf_addstr(&ctx->message, "\n");
- } else {
- strbuf_addstr(&ctx->message, "Revert \"");
- strbuf_addstr(&ctx->message, msg.subject);
- strbuf_addstr(&ctx->message, "\"\n");
- }
- strbuf_addstr(&ctx->message, "\nThis reverts commit ");
- refer_to_commit(opts, &ctx->message, commit);
-
- if (commit->parents && commit->parents->next) {
- strbuf_addstr(&ctx->message, ", reversing\nchanges made to ");
- refer_to_commit(opts, &ctx->message, parent);
- }
- strbuf_addstr(&ctx->message, ".\n");
+ sequencer_format_revert_message(r, msg.subject, commit,
+ parent,
+ opts->commit_use_reference,
+ &ctx->message);
} else {
const char *p;
@@ -5628,6 +5605,43 @@ out:
return res;
}
+void sequencer_format_revert_message(struct repository *r,
+ const char *subject,
+ const struct commit *commit,
+ const struct commit *parent,
+ bool use_commit_reference,
+ struct strbuf *message)
+{
+ const char *orig_subject;
+
+ if (use_commit_reference) {
+ strbuf_commented_addf(message, comment_line_str,
+ "*** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***");
+ } else if (skip_prefix(subject, "Revert \"", &orig_subject) &&
+ /*
+ * We don't touch pre-existing repeated reverts, because
+ * theoretically these can be nested arbitrarily deeply,
+ * thus requiring excessive complexity to deal with.
+ */
+ !starts_with(orig_subject, "Revert \"")) {
+ strbuf_addstr(message, "Reapply \"");
+ strbuf_addstr(message, orig_subject);
+ strbuf_addstr(message, "\n");
+ } else {
+ strbuf_addstr(message, "Revert \"");
+ strbuf_addstr(message, subject);
+ strbuf_addstr(message, "\"\n");
+ }
+ strbuf_addstr(message, "\nThis reverts commit ");
+ refer_to_commit(r, message, commit, use_commit_reference);
+
+ if (commit->parents && commit->parents->next) {
+ strbuf_addstr(message, ", reversing\nchanges made to ");
+ refer_to_commit(r, message, parent, use_commit_reference);
+ }
+ strbuf_addstr(message, ".\n");
+}
+
void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag)
{
unsigned no_dup_sob = flag & APPEND_SIGNOFF_DEDUP;
diff --git a/sequencer.h b/sequencer.h
index bea20da085..a6fa670c7c 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -274,4 +274,17 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence)
*/
int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs);
+/*
+ * Format a revert commit message with appropriate 'Revert "<subject>"' or
+ * 'Reapply "<subject>"' prefix and 'This reverts commit <ref>.' body.
+ * When use_commit_reference is set, <ref> is an abbreviated hash with
+ * subject and date; otherwise the full hex hash is used.
+ */
+void sequencer_format_revert_message(struct repository *r,
+ const char *subject,
+ const struct commit *commit,
+ const struct commit *parent,
+ bool use_commit_reference,
+ struct strbuf *message);
+
#endif /* SEQUENCER_H */
diff --git a/string-list.c b/string-list.c
index fffa2ad4b6..d260b873c8 100644
--- a/string-list.c
+++ b/string-list.c
@@ -281,6 +281,15 @@ void unsorted_string_list_delete_item(struct string_list *list, int i, int free_
list->nr--;
}
+void unsorted_string_list_remove(struct string_list *list, const char *str,
+ int free_util)
+{
+ struct string_list_item *item = unsorted_string_list_lookup(list, str);
+ if (item)
+ unsorted_string_list_delete_item(list, item - list->items,
+ free_util);
+}
+
/*
* append a substring [p..end] to list; return number of things it
* appended to the list.
diff --git a/string-list.h b/string-list.h
index 3ad862a187..b86ee7c099 100644
--- a/string-list.h
+++ b/string-list.h
@@ -266,6 +266,14 @@ struct string_list_item *unsorted_string_list_lookup(struct string_list *list,
void unsorted_string_list_delete_item(struct string_list *list, int i, int free_util);
/**
+ * Remove the first item matching `str` from an unsorted string_list.
+ * No-op if `str` is not found. If `free_util` is non-zero, the `util`
+ * pointer of the removed item is freed before deletion.
+ */
+void unsorted_string_list_remove(struct string_list *list, const char *str,
+ int free_util);
+
+/**
* Split string into substrings on characters in `delim` and append the
* substrings to `list`. The input string is not modified.
* list->strdup_strings must be set, as new memory needs to be
diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh
index b1583e9ef9..96749fc06d 100755
--- a/t/t1800-hook.sh
+++ b/t/t1800-hook.sh
@@ -25,18 +25,47 @@ test_expect_success 'git hook usage' '
test_expect_code 129 git hook &&
test_expect_code 129 git hook run &&
test_expect_code 129 git hook run -h &&
- test_expect_code 129 git hook list -h &&
test_expect_code 129 git hook run --unknown 2>err &&
test_expect_code 129 git hook list &&
test_expect_code 129 git hook list -h &&
grep "unknown option" err
'
+test_expect_success 'git hook list: unknown hook name is rejected' '
+ test_must_fail git hook list prereceive 2>err &&
+ test_grep "unknown hook event" err
+'
+
+test_expect_success 'git hook run: unknown hook name is rejected' '
+ test_must_fail git hook run prereceive 2>err &&
+ test_grep "unknown hook event" err
+'
+
+test_expect_success 'git hook list: known hook name is accepted' '
+ test_must_fail git hook list pre-receive 2>err &&
+ test_grep ! "unknown hook event" err
+'
+
+test_expect_success 'git hook run: known hook name is accepted' '
+ git hook run --ignore-missing pre-receive 2>err &&
+ test_grep ! "unknown hook event" err
+'
+
+test_expect_success 'git hook run: --allow-unknown-hook-name overrides rejection' '
+ git hook run --allow-unknown-hook-name --ignore-missing custom-hook 2>err &&
+ test_grep ! "unknown hook event" err
+'
+
+test_expect_success 'git hook list: --allow-unknown-hook-name overrides rejection' '
+ test_must_fail git hook list --allow-unknown-hook-name custom-hook 2>err &&
+ test_grep ! "unknown hook event" err
+'
+
test_expect_success 'git hook list: nonexistent hook' '
cat >stderr.expect <<-\EOF &&
- warning: No hooks found for event '\''test-hook'\''
+ warning: no hooks found for event '\''test-hook'\''
EOF
- test_expect_code 1 git hook list test-hook 2>stderr.actual &&
+ test_expect_code 1 git hook list --allow-unknown-hook-name test-hook 2>stderr.actual &&
test_cmp stderr.expect stderr.actual
'
@@ -48,7 +77,7 @@ test_expect_success 'git hook list: traditional hook from hookdir' '
cat >expect <<-\EOF &&
hook from hookdir
EOF
- git hook list test-hook >actual &&
+ git hook list --allow-unknown-hook-name test-hook >actual &&
test_cmp expect actual
'
@@ -57,7 +86,7 @@ test_expect_success 'git hook list: configured hook' '
test_config hook.myhook.event test-hook --add &&
echo "myhook" >expect &&
- git hook list test-hook >actual &&
+ git hook list --allow-unknown-hook-name test-hook >actual &&
test_cmp expect actual
'
@@ -69,7 +98,7 @@ test_expect_success 'git hook list: -z shows NUL-terminated output' '
test_config hook.myhook.event test-hook --add &&
printf "myhookQhook from hookdirQ" >expect &&
- git hook list -z test-hook >actual.raw &&
+ git hook list --allow-unknown-hook-name -z test-hook >actual.raw &&
nul_to_q <actual.raw >actual &&
test_cmp expect actual
'
@@ -78,12 +107,12 @@ test_expect_success 'git hook run: nonexistent hook' '
cat >stderr.expect <<-\EOF &&
error: cannot find a hook named test-hook
EOF
- test_expect_code 1 git hook run test-hook 2>stderr.actual &&
+ test_expect_code 1 git hook run --allow-unknown-hook-name test-hook 2>stderr.actual &&
test_cmp stderr.expect stderr.actual
'
test_expect_success 'git hook run: nonexistent hook with --ignore-missing' '
- git hook run --ignore-missing does-not-exist 2>stderr.actual &&
+ git hook run --allow-unknown-hook-name --ignore-missing does-not-exist 2>stderr.actual &&
test_must_be_empty stderr.actual
'
@@ -95,7 +124,7 @@ test_expect_success 'git hook run: basic' '
cat >expect <<-\EOF &&
Test hook
EOF
- git hook run test-hook 2>actual &&
+ git hook run --allow-unknown-hook-name test-hook 2>actual &&
test_cmp expect actual
'
@@ -109,7 +138,7 @@ test_expect_success 'git hook run: stdout and stderr both write to our stderr' '
Will end up on stderr
Will end up on stderr
EOF
- git hook run test-hook >stdout.actual 2>stderr.actual &&
+ git hook run --allow-unknown-hook-name test-hook >stdout.actual 2>stderr.actual &&
test_cmp stderr.expect stderr.actual &&
test_must_be_empty stdout.actual
'
@@ -121,12 +150,12 @@ do
exit $code
EOF
- test_expect_code $code git hook run test-hook
+ test_expect_code $code git hook run --allow-unknown-hook-name test-hook
'
done
test_expect_success 'git hook run arg u ments without -- is not allowed' '
- test_expect_code 129 git hook run test-hook arg u ments
+ test_expect_code 129 git hook run --allow-unknown-hook-name test-hook arg u ments
'
test_expect_success 'git hook run -- pass arguments' '
@@ -140,7 +169,7 @@ test_expect_success 'git hook run -- pass arguments' '
u ments
EOF
- git hook run test-hook -- arg "u ments" 2>actual &&
+ git hook run --allow-unknown-hook-name test-hook -- arg "u ments" 2>actual &&
test_cmp expect actual
'
@@ -149,12 +178,12 @@ test_expect_success 'git hook run: out-of-repo runs execute global hooks' '
test_config_global hook.global-hook.command "echo no repo no problems" --add &&
echo "global-hook" >expect &&
- nongit git hook list test-hook >actual &&
+ nongit git hook list --allow-unknown-hook-name test-hook >actual &&
test_cmp expect actual &&
echo "no repo no problems" >expect &&
- nongit git hook run test-hook 2>actual &&
+ nongit git hook run --allow-unknown-hook-name test-hook 2>actual &&
test_cmp expect actual
'
@@ -179,11 +208,11 @@ test_expect_success 'git -c core.hooksPath=<PATH> hook run' '
# Test various ways of specifying the path. See also
# t1350-config-hooks-path.sh
>actual &&
- git hook run test-hook -- ignored 2>>actual &&
- git -c core.hooksPath=my-hooks hook run test-hook -- one 2>>actual &&
- git -c core.hooksPath=my-hooks/ hook run test-hook -- two 2>>actual &&
- git -c core.hooksPath="$PWD/my-hooks" hook run test-hook -- three 2>>actual &&
- git -c core.hooksPath="$PWD/my-hooks/" hook run test-hook -- four 2>>actual &&
+ git hook run --allow-unknown-hook-name test-hook -- ignored 2>>actual &&
+ git -c core.hooksPath=my-hooks hook run --allow-unknown-hook-name test-hook -- one 2>>actual &&
+ git -c core.hooksPath=my-hooks/ hook run --allow-unknown-hook-name test-hook -- two 2>>actual &&
+ git -c core.hooksPath="$PWD/my-hooks" hook run --allow-unknown-hook-name test-hook -- three 2>>actual &&
+ git -c core.hooksPath="$PWD/my-hooks/" hook run --allow-unknown-hook-name test-hook -- four 2>>actual &&
test_cmp expect actual
'
@@ -263,7 +292,7 @@ test_expect_success 'hook can be configured for multiple events' '
# 'ghi' should be included in both 'pre-commit' and 'test-hook'
git hook list pre-commit >actual &&
grep "ghi" actual &&
- git hook list test-hook >actual &&
+ git hook list --allow-unknown-hook-name test-hook >actual &&
grep "ghi" actual
'
@@ -337,15 +366,15 @@ test_expect_success 'stdin to multiple hooks' '
b3
EOF
- git hook run --to-stdin=input test-hook 2>actual &&
+ git hook run --allow-unknown-hook-name --to-stdin=input test-hook 2>actual &&
test_cmp expected actual
'
test_expect_success 'rejects hooks with no commands configured' '
test_config hook.broken.event "test-hook" &&
- test_must_fail git hook list test-hook 2>actual &&
+ test_must_fail git hook list --allow-unknown-hook-name test-hook 2>actual &&
test_grep "hook.broken.command" actual &&
- test_must_fail git hook run test-hook 2>actual &&
+ test_must_fail git hook run --allow-unknown-hook-name test-hook 2>actual &&
test_grep "hook.broken.command" actual
'
@@ -354,11 +383,19 @@ test_expect_success 'disabled hook is not run' '
test_config hook.skipped.command "echo \"Should not run\"" &&
test_config hook.skipped.enabled false &&
- git hook run --ignore-missing test-hook 2>actual &&
+ git hook run --allow-unknown-hook-name --ignore-missing test-hook 2>actual &&
test_must_be_empty actual
'
-test_expect_success 'disabled hook does not appear in git hook list' '
+test_expect_success 'disabled hook with no command warns' '
+ test_config hook.nocommand.event "pre-commit" &&
+ test_config hook.nocommand.enabled false &&
+
+ git hook list pre-commit 2>actual &&
+ test_grep "disabled hook.*nocommand.*no command configured" actual
+'
+
+test_expect_success 'disabled hook appears as disabled in git hook list' '
test_config hook.active.event "pre-commit" &&
test_config hook.active.command "echo active" &&
test_config hook.inactive.event "pre-commit" &&
@@ -366,8 +403,27 @@ test_expect_success 'disabled hook does not appear in git hook list' '
test_config hook.inactive.enabled false &&
git hook list pre-commit >actual &&
- test_grep "active" actual &&
- test_grep ! "inactive" actual
+ test_grep "^active$" actual &&
+ test_grep "^disabled inactive$" actual
+'
+
+test_expect_success 'disabled hook shows scope with --show-scope' '
+ test_config hook.myhook.event "pre-commit" &&
+ test_config hook.myhook.command "echo hi" &&
+ test_config hook.myhook.enabled false &&
+
+ git hook list --show-scope pre-commit >actual &&
+ test_grep "^local disabled myhook$" actual
+'
+
+test_expect_success 'disabled configured hook is not reported as existing by hook_exists' '
+ test_when_finished "rm -f git-bugreport-hook-exists-test.txt" &&
+ test_config hook.linter.event "pre-commit" &&
+ test_config hook.linter.command "echo lint" &&
+ test_config hook.linter.enabled false &&
+
+ git bugreport -s hook-exists-test &&
+ test_grep ! "pre-commit" git-bugreport-hook-exists-test.txt
'
test_expect_success 'globally disabled hook can be re-enabled locally' '
@@ -377,10 +433,59 @@ test_expect_success 'globally disabled hook can be re-enabled locally' '
test_config hook.global-hook.enabled true &&
echo "global-hook ran" >expected &&
- git hook run test-hook 2>actual &&
+ git hook run --allow-unknown-hook-name test-hook 2>actual &&
+ test_cmp expected actual
+'
+
+test_expect_success 'configured hooks run before hookdir hook' '
+ setup_hookdir &&
+ test_config hook.first.event "pre-commit" &&
+ test_config hook.first.command "echo first" &&
+ test_config hook.second.event "pre-commit" &&
+ test_config hook.second.command "echo second" &&
+
+ cat >expected <<-\EOF &&
+ first
+ second
+ hook from hookdir
+ EOF
+
+ git hook list pre-commit >actual &&
+ test_cmp expected actual &&
+
+ # "Legacy Hook" is the output of the hookdir pre-commit script
+ # written by setup_hookdir() above.
+ cat >expected <<-\EOF &&
+ first
+ second
+ "Legacy Hook"
+ EOF
+
+ git hook run pre-commit 2>actual &&
test_cmp expected actual
'
+test_expect_success 'git hook list --show-scope shows config scope' '
+ setup_hookdir &&
+ test_config_global hook.global-hook.command "echo global" &&
+ test_config_global hook.global-hook.event pre-commit --add &&
+ test_config hook.local-hook.command "echo local" &&
+ test_config hook.local-hook.event pre-commit --add &&
+
+ cat >expected <<-\EOF &&
+ global global-hook
+ local local-hook
+ hook from hookdir
+ EOF
+ git hook list --show-scope pre-commit >actual &&
+ test_cmp expected actual &&
+
+ # without --show-scope the scope must not appear
+ git hook list pre-commit >actual &&
+ test_grep ! "^global " actual &&
+ test_grep ! "^local " actual
+'
+
test_expect_success 'git hook run a hook with a bad shebang' '
test_when_finished "rm -rf bad-hooks" &&
mkdir bad-hooks &&
@@ -388,7 +493,7 @@ test_expect_success 'git hook run a hook with a bad shebang' '
test_expect_code 1 git \
-c core.hooksPath=bad-hooks \
- hook run test-hook >out 2>err &&
+ hook run --allow-unknown-hook-name test-hook >out 2>err &&
test_must_be_empty out &&
# TODO: We should emit the same (or at least a more similar)
@@ -412,7 +517,7 @@ test_expect_success 'stdin to hooks' '
EOF
echo hello >input &&
- git hook run --to-stdin=input test-hook 2>actual &&
+ git hook run --allow-unknown-hook-name --to-stdin=input test-hook 2>actual &&
test_cmp expect actual
'
diff --git a/t/t2400-worktree-add.sh b/t/t2400-worktree-add.sh
index 023e1301c8..58b4445cc4 100755
--- a/t/t2400-worktree-add.sh
+++ b/t/t2400-worktree-add.sh
@@ -987,7 +987,7 @@ test_dwim_orphan () {
then
test_must_be_empty actual
else
- grep "$info_text" actual
+ test_grep "$info_text" actual
fi
elif [ "$outcome" = "no_infer" ]
then
@@ -996,39 +996,35 @@ test_dwim_orphan () {
then
test_must_be_empty actual
else
- ! grep "$info_text" actual
+ test_grep ! "$info_text" actual
fi
elif [ "$outcome" = "fetch_error" ]
then
test_must_fail git $dashc_args worktree add $args 2>actual &&
- grep "$fetch_error_text" actual
+ test_grep "$fetch_error_text" actual
elif [ "$outcome" = "fatal_orphan_bad_combo" ]
then
test_must_fail git $dashc_args worktree add $args 2>actual &&
if [ $use_quiet -eq 1 ]
then
- ! grep "$info_text" actual
+ test_grep ! "$info_text" actual
else
- grep "$info_text" actual
+ test_grep "$info_text" actual
fi &&
- grep "$bad_combo_regex" actual
+ test_grep "$bad_combo_regex" actual
elif [ "$outcome" = "warn_bad_head" ]
then
test_must_fail git $dashc_args worktree add $args 2>actual &&
if [ $use_quiet -eq 1 ]
then
- grep "$invalid_ref_regex" actual &&
- ! grep "$orphan_hint" actual
+ test_grep "$invalid_ref_regex" actual &&
+ test_grep ! "$orphan_hint" actual
else
- headpath=$(git $dashc_args rev-parse --path-format=absolute --git-path HEAD) &&
- headcontents=$(cat "$headpath") &&
- grep "HEAD points to an invalid (or orphaned) reference" actual &&
- grep "HEAD path: .$headpath." actual &&
- grep "HEAD contents: .$headcontents." actual &&
- grep "$orphan_hint" actual &&
- ! grep "$info_text" actual
+ test_grep "HEAD points to an invalid (or orphaned) reference" actual &&
+ test_grep "$orphan_hint" actual &&
+ test_grep ! "$info_text" actual
fi &&
- grep "$invalid_ref_regex" actual
+ test_grep "$invalid_ref_regex" actual
else
# Unreachable
false
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index a03f8f9293..217f6fb292 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -74,8 +74,8 @@ test_expect_success '--onto with invalid commit-ish' '
test_cmp expect actual
'
-test_expect_success 'option --onto or --advance is mandatory' '
- echo "error: option --onto or --advance is mandatory" >expect &&
+test_expect_success 'exactly one of --onto, --advance, or --revert is required' '
+ echo "error: exactly one of --onto, --advance, or --revert is required" >expect &&
test_might_fail git replay -h >>expect &&
test_must_fail git replay topic1..topic2 2>actual &&
test_cmp expect actual
@@ -87,16 +87,14 @@ test_expect_success 'no base or negative ref gives no-replaying down to root err
test_cmp expect actual
'
-test_expect_success 'options --advance and --contained cannot be used together' '
- printf "fatal: options ${SQ}--advance${SQ} " >expect &&
- printf "and ${SQ}--contained${SQ} cannot be used together\n" >>expect &&
+test_expect_success '--advance and --contained cannot be used together' '
test_must_fail git replay --advance=main --contained \
topic1..topic2 2>actual &&
- test_cmp expect actual
+ test_grep "cannot be used together" actual
'
test_expect_success 'cannot advance target ... ordering would be ill-defined' '
- echo "fatal: cannot advance target with multiple sources because ordering would be ill-defined" >expect &&
+ echo "fatal: ${SQ}--advance${SQ} cannot be used with multiple revision ranges because the ordering would be ill-defined" >expect &&
test_must_fail git replay --advance=main main topic1 topic2 2>actual &&
test_cmp expect actual
'
@@ -398,4 +396,103 @@ test_expect_success 'invalid replay.refAction value' '
test_grep "invalid.*replay.refAction.*value" error
'
+test_expect_success 'argument to --revert must be a reference' '
+ echo "fatal: argument to --revert must be a reference" >expect &&
+ oid=$(git rev-parse main) &&
+ test_must_fail git replay --revert=$oid topic1..topic2 2>actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'cannot revert with multiple sources' '
+ echo "fatal: ${SQ}--revert${SQ} cannot be used with multiple revision ranges because the ordering would be ill-defined" >expect &&
+ test_must_fail git replay --revert main main topic1 topic2 2>actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'using replay --revert to revert commits' '
+ # Reuse existing topic4 branch (has commits I and J on top of main)
+ START=$(git rev-parse topic4) &&
+ test_when_finished "git branch -f topic4 $START" &&
+
+ # Revert commits I and J
+ git replay --revert topic4 topic4~2..topic4 &&
+
+ # Verify the revert commits were created (newest-first ordering
+ # means J is reverted first, then I on top)
+ git log --format=%s -4 topic4 >actual &&
+ cat >expect <<-\EOF &&
+ Revert "I"
+ Revert "J"
+ J
+ I
+ EOF
+ test_cmp expect actual &&
+
+ # Verify commit message format includes hash (tip is Revert "I")
+ test_commit_message topic4 <<-EOF &&
+ Revert "I"
+
+ This reverts commit $(git rev-parse I).
+ EOF
+
+ # Verify reflog message
+ git reflog topic4 -1 --format=%gs >reflog-msg &&
+ echo "replay --revert topic4" >expect-reflog &&
+ test_cmp expect-reflog reflog-msg
+'
+
+test_expect_success 'using replay --revert in bare repo' '
+ # Reuse existing topic4 in bare repo
+ START=$(git -C bare rev-parse topic4) &&
+ test_when_finished "git -C bare update-ref refs/heads/topic4 $START" &&
+
+ # Revert commit J in bare repo
+ git -C bare replay --revert topic4 topic4~1..topic4 &&
+
+ # Verify revert was created
+ git -C bare log -1 --format=%s topic4 >actual &&
+ echo "Revert \"J\"" >expect &&
+ test_cmp expect actual
+'
+
+test_expect_success 'revert of revert uses Reapply' '
+ # Use topic4 and first revert J, then revert the revert
+ START=$(git rev-parse topic4) &&
+ test_when_finished "git branch -f topic4 $START" &&
+
+ # First revert J
+ git replay --revert topic4 topic4~1..topic4 &&
+ REVERT_J=$(git rev-parse topic4) &&
+
+ # Now revert the revert - should become Reapply
+ git replay --revert topic4 topic4~1..topic4 &&
+
+ # Verify Reapply prefix and message format
+ test_commit_message topic4 <<-EOF
+ Reapply "J"
+
+ This reverts commit $REVERT_J.
+ EOF
+'
+
+test_expect_success 'git replay --revert with conflict' '
+ # conflict branch has C.conflict which conflicts with topic1s C
+ test_expect_code 1 git replay --revert conflict B..topic1
+'
+
+test_expect_success 'git replay --revert incompatible with --contained' '
+ test_must_fail git replay --revert topic4 --contained topic4~1..topic4 2>error &&
+ test_grep "cannot be used together" error
+'
+
+test_expect_success 'git replay --revert incompatible with --onto' '
+ test_must_fail git replay --revert topic4 --onto main topic4~1..topic4 2>error &&
+ test_grep "cannot be used together" error
+'
+
+test_expect_success 'git replay --revert incompatible with --advance' '
+ test_must_fail git replay --revert topic4 --advance main topic4~1..topic4 2>error &&
+ test_grep "cannot be used together" error
+'
+
test_done
diff --git a/t/t4014-format-patch.sh b/t/t4014-format-patch.sh
index bcdb944017..7517094bd6 100755
--- a/t/t4014-format-patch.sh
+++ b/t/t4014-format-patch.sh
@@ -380,6 +380,131 @@ test_expect_success 'filename limit applies only to basename' '
done
'
+test_expect_success 'cover letter with subject, author and count' '
+ rm -rf patches &&
+ test_when_finished "git reset --hard HEAD~1" &&
+ test_when_finished "rm -rf patches test_file" &&
+ touch test_file &&
+ git add test_file &&
+ git commit -m "This is a subject" &&
+ git format-patch --commit-list-format="log:[%(count)/%(total)] %s (%an)" \
+ -o patches HEAD~1 &&
+ test_grep "^\[1/1\] This is a subject (A U Thor)$" patches/0000-cover-letter.patch
+'
+
+test_expect_success 'cover letter with custom format no prefix' '
+ rm -rf patches &&
+ test_when_finished "git reset --hard HEAD~1" &&
+ test_when_finished "rm -rf patches test_file" &&
+ touch test_file &&
+ git add test_file &&
+ git commit -m "This is a subject" &&
+ git format-patch --commit-list-format="[%(count)/%(total)] %s (%an)" \
+ -o patches HEAD~1 &&
+ test_grep "^\[1/1\] This is a subject (A U Thor)$" patches/0000-cover-letter.patch
+'
+
+test_expect_success 'cover letter fail when no prefix and no placeholder' '
+ rm -rf patches &&
+ test_when_finished "git reset --hard HEAD~1" &&
+ test_when_finished "rm -rf patches test_file err" &&
+ touch test_file &&
+ git add test_file &&
+ git commit -m "This is a subject" &&
+ test_must_fail git format-patch --commit-list-format="this should fail" \
+ -o patches HEAD~1 2>err &&
+ test_grep "is not a valid format string" err
+'
+
+test_expect_success 'cover letter modern format' '
+ test_when_finished "git reset --hard HEAD~1" &&
+ test_when_finished "rm -rf patches test_file" &&
+ touch test_file &&
+ git add test_file &&
+ git commit -m "This is a subject" &&
+ git format-patch --commit-list-format="modern" -o patches HEAD~1 &&
+ test_grep "^\[1/1\] This is a subject$" patches/0000-cover-letter.patch
+'
+
+test_expect_success 'cover letter shortlog format' '
+ test_when_finished "git reset --hard HEAD~1" &&
+ test_when_finished "rm -rf expect patches result test_file" &&
+ cat >expect <<-"EOF" &&
+ A U Thor (1):
+ This is a subject
+ EOF
+ touch test_file &&
+ git add test_file &&
+ git commit -m "This is a subject" &&
+ git format-patch --commit-list-format=shortlog -o patches HEAD~1 &&
+ grep -E -A 1 "^A U Thor \([[:digit:]]+\):$" patches/0000-cover-letter.patch >result &&
+ cat result &&
+ test_cmp expect result
+'
+
+test_expect_success 'no cover letter but with format specified' '
+ test_when_finished "git reset --hard HEAD~1" &&
+ test_when_finished "rm -rf patches result test_file" &&
+ touch test_file &&
+ git add test_file &&
+ git commit -m "This is a subject" &&
+ git format-patch --no-cover-letter --commit-list-format="[%(count)] %s" -o patches HEAD~1 &&
+ test_path_is_missing patches/0000-cover-letter.patch
+'
+
+test_expect_success 'cover letter config with count, subject and author' '
+ test_when_finished "rm -rf patches result" &&
+ test_when_finished "git config unset format.coverletter" &&
+ test_when_finished "git config unset format.commitlistformat" &&
+ git config set format.coverletter true &&
+ git config set format.commitlistformat "log:[%(count)/%(total)] %s (%an)" &&
+ git format-patch -o patches HEAD~2 &&
+ grep -E "^[[[:digit:]]+/[[:digit:]]+] .* \(A U Thor\)" patches/0000-cover-letter.patch >result &&
+ test_line_count = 2 result
+'
+
+test_expect_success 'cover letter config with count and author' '
+ test_when_finished "rm -rf patches result" &&
+ test_when_finished "git config unset format.coverletter" &&
+ test_when_finished "git config unset format.commitlistformat" &&
+ git config set format.coverletter true &&
+ git config set format.commitlistformat "log:[%(count)/%(total)] (%an)" &&
+ git format-patch -o patches HEAD~2 &&
+ grep -E "^[[[:digit:]]+/[[:digit:]]+] \(A U Thor\)" patches/0000-cover-letter.patch >result &&
+ test_line_count = 2 result
+'
+
+test_expect_success 'cover letter config commitlistformat set to modern' '
+ test_when_finished "rm -rf patches result" &&
+ test_when_finished "git config unset format.coverletter" &&
+ test_when_finished "git config unset format.commitlistformat" &&
+ git config set format.coverletter true &&
+ git config set format.commitlistformat modern &&
+ git format-patch -o patches HEAD~2 &&
+ grep -E "^[[[:digit:]]+/[[:digit:]]+] .*$" patches/0000-cover-letter.patch >result &&
+ test_line_count = 2 result
+'
+
+test_expect_success 'cover letter config commitlistformat set to shortlog' '
+ test_when_finished "rm -rf patches result" &&
+ test_when_finished "git config unset format.coverletter" &&
+ test_when_finished "git config unset format.commitlistformat" &&
+ git config set format.coverletter true &&
+ git config set format.commitlistformat shortlog &&
+ git format-patch -o patches HEAD~2 &&
+ grep -E "^A U Thor \([[:digit:]]+\)" patches/0000-cover-letter.patch >result &&
+ test_line_count = 1 result
+'
+
+test_expect_success 'cover letter config commitlistformat not set' '
+ test_when_finished "rm -rf patches result" &&
+ test_when_finished "git config unset format.coverletter" &&
+ git config set format.coverletter true &&
+ git format-patch -o patches HEAD~2 &&
+ grep -E "^A U Thor \([[:digit:]]+\)" patches/0000-cover-letter.patch >result &&
+ test_line_count = 1 result
+'
+
test_expect_success 'reroll count' '
rm -fr patches &&
git format-patch -o patches --cover-letter --reroll-count 4 main..side >list &&
diff --git a/t/t5620-backfill.sh b/t/t5620-backfill.sh
index 58c81556e7..2c347a91fe 100755
--- a/t/t5620-backfill.sh
+++ b/t/t5620-backfill.sh
@@ -7,6 +7,14 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
. ./test-lib.sh
+test_expect_success 'backfill rejects unexpected arguments' '
+ test_must_fail git backfill unexpected-arg 2>err &&
+ test_grep "ambiguous argument .*unexpected-arg" err &&
+
+ test_must_fail git backfill --all --unexpected-arg --first-parent 2>err &&
+ test_grep "unrecognized argument: --unexpected-arg" err
+'
+
# We create objects in the 'src' repo.
test_expect_success 'setup repo for object creation' '
echo "{print \$1}" >print_1.awk &&
@@ -15,7 +23,7 @@ test_expect_success 'setup repo for object creation' '
git init src &&
mkdir -p src/a/b/c &&
- mkdir -p src/d/e &&
+ mkdir -p src/d/f &&
for i in 1 2
do
@@ -26,8 +34,9 @@ test_expect_success 'setup repo for object creation' '
echo "Version $i of file a/b/$n" > src/a/b/file.$n.txt &&
echo "Version $i of file a/b/c/$n" > src/a/b/c/file.$n.txt &&
echo "Version $i of file d/$n" > src/d/file.$n.txt &&
- echo "Version $i of file d/e/$n" > src/d/e/file.$n.txt &&
+ echo "Version $i of file d/f/$n" > src/d/f/file.$n.txt &&
git -C src add . &&
+ test_tick &&
git -C src commit -m "Iteration $n" || return 1
done
done
@@ -41,6 +50,53 @@ test_expect_success 'setup bare clone for server' '
git -C srv.bare config --local uploadpack.allowanysha1inwant 1
'
+# Create a version of the repo with branches for testing revision
+# arguments like --all, --first-parent, and --since.
+#
+# main: 8 commits (linear) + merge of side branch
+# 48 original blobs + 4 side blobs = 52 blobs from main HEAD
+# side: 2 commits adding s/file.{1,2}.txt (v1, v2), merged into main
+# other: 1 commit adding o/file.{1,2}.txt (not merged)
+# 54 total blobs reachable from --all
+test_expect_success 'setup branched repo for revision tests' '
+ git clone src src-revs &&
+
+ # Side branch from tip of main with unique files
+ git -C src-revs checkout -b side HEAD &&
+ mkdir -p src-revs/s &&
+ echo "Side version 1 of file 1" >src-revs/s/file.1.txt &&
+ echo "Side version 1 of file 2" >src-revs/s/file.2.txt &&
+ test_tick &&
+ git -C src-revs add . &&
+ git -C src-revs commit -m "Side commit 1" &&
+
+ echo "Side version 2 of file 1" >src-revs/s/file.1.txt &&
+ echo "Side version 2 of file 2" >src-revs/s/file.2.txt &&
+ test_tick &&
+ git -C src-revs add . &&
+ git -C src-revs commit -m "Side commit 2" &&
+
+ # Merge side into main
+ git -C src-revs checkout main &&
+ test_tick &&
+ git -C src-revs merge side --no-ff -m "Merge side branch" &&
+
+ # Other branch (not merged) for --all testing
+ git -C src-revs checkout -b other main~1 &&
+ mkdir -p src-revs/o &&
+ echo "Other content 1" >src-revs/o/file.1.txt &&
+ echo "Other content 2" >src-revs/o/file.2.txt &&
+ test_tick &&
+ git -C src-revs add . &&
+ git -C src-revs commit -m "Other commit" &&
+
+ git -C src-revs checkout main &&
+
+ git clone --bare "file://$(pwd)/src-revs" srv-revs.bare &&
+ git -C srv-revs.bare config --local uploadpack.allowfilter 1 &&
+ git -C srv-revs.bare config --local uploadpack.allowanysha1inwant 1
+'
+
# do basic partial clone from "srv.bare"
test_expect_success 'do partial clone 1, backfill gets all objects' '
git clone --no-checkout --filter=blob:none \
@@ -176,6 +232,157 @@ test_expect_success 'backfill --sparse without cone mode (negative)' '
test_line_count = 12 missing
'
+test_expect_success 'backfill with revision range' '
+ test_when_finished rm -rf backfill-revs &&
+ git clone --no-checkout --filter=blob:none \
+ --single-branch --branch=main \
+ "file://$(pwd)/srv.bare" backfill-revs &&
+
+ # No blobs yet
+ git -C backfill-revs rev-list --quiet --objects --missing=print HEAD >missing &&
+ test_line_count = 48 missing &&
+
+ git -C backfill-revs backfill HEAD~2..HEAD &&
+
+ # 30 objects downloaded.
+ git -C backfill-revs rev-list --quiet --objects --missing=print HEAD >missing &&
+ test_line_count = 18 missing
+'
+
+test_expect_success 'backfill with revisions over stdin' '
+ test_when_finished rm -rf backfill-revs &&
+ git clone --no-checkout --filter=blob:none \
+ --single-branch --branch=main \
+ "file://$(pwd)/srv.bare" backfill-revs &&
+
+ # No blobs yet
+ git -C backfill-revs rev-list --quiet --objects --missing=print HEAD >missing &&
+ test_line_count = 48 missing &&
+
+ cat >in <<-EOF &&
+ HEAD
+ ^HEAD~2
+ EOF
+
+ git -C backfill-revs backfill --stdin <in &&
+
+ # 30 objects downloaded.
+ git -C backfill-revs rev-list --quiet --objects --missing=print HEAD >missing &&
+ test_line_count = 18 missing
+'
+
+test_expect_success 'backfill with prefix pathspec' '
+ test_when_finished rm -rf backfill-path &&
+ git clone --bare --filter=blob:none \
+ --single-branch --branch=main \
+ "file://$(pwd)/srv.bare" backfill-path &&
+
+ # No blobs yet
+ git -C backfill-path rev-list --quiet --objects --missing=print HEAD >missing &&
+ test_line_count = 48 missing &&
+
+ git -C backfill-path backfill HEAD -- d/f 2>err &&
+ test_must_be_empty err &&
+
+ git -C backfill-path rev-list --quiet --objects --missing=print HEAD >missing &&
+ test_line_count = 40 missing
+'
+
+test_expect_success 'backfill with multiple pathspecs' '
+ test_when_finished rm -rf backfill-path &&
+ git clone --bare --filter=blob:none \
+ --single-branch --branch=main \
+ "file://$(pwd)/srv.bare" backfill-path &&
+
+ # No blobs yet
+ git -C backfill-path rev-list --quiet --objects --missing=print HEAD >missing &&
+ test_line_count = 48 missing &&
+
+ git -C backfill-path backfill HEAD -- d/f a 2>err &&
+ test_must_be_empty err &&
+
+ git -C backfill-path rev-list --quiet --objects --missing=print HEAD >missing &&
+ test_line_count = 16 missing
+'
+
+test_expect_success 'backfill with wildcard pathspec' '
+ test_when_finished rm -rf backfill-path &&
+ git clone --bare --filter=blob:none \
+ --single-branch --branch=main \
+ "file://$(pwd)/srv.bare" backfill-path &&
+
+ # No blobs yet
+ git -C backfill-path rev-list --quiet --objects --missing=print HEAD >missing &&
+ test_line_count = 48 missing &&
+
+ git -C backfill-path backfill HEAD -- "d/file.*.txt" 2>err &&
+ test_must_be_empty err &&
+
+ git -C backfill-path rev-list --quiet --objects --missing=print HEAD >missing &&
+ test_line_count = 40 missing
+'
+
+test_expect_success 'backfill with --all' '
+ test_when_finished rm -rf backfill-all &&
+ git clone --no-checkout --filter=blob:none \
+ "file://$(pwd)/srv-revs.bare" backfill-all &&
+
+ # All blobs from all refs are missing
+ git -C backfill-all rev-list --quiet --objects --all --missing=print >missing &&
+ test_line_count = 54 missing &&
+
+ # Backfill from HEAD gets main blobs only
+ git -C backfill-all backfill HEAD &&
+
+ # Other branch blobs still missing
+ git -C backfill-all rev-list --quiet --objects --all --missing=print >missing &&
+ test_line_count = 2 missing &&
+
+ # Backfill with --all gets everything
+ git -C backfill-all backfill --all &&
+
+ git -C backfill-all rev-list --quiet --objects --all --missing=print >missing &&
+ test_line_count = 0 missing
+'
+
+test_expect_success 'backfill with --first-parent' '
+ test_when_finished rm -rf backfill-fp &&
+ git clone --no-checkout --filter=blob:none \
+ --single-branch --branch=main \
+ "file://$(pwd)/srv-revs.bare" backfill-fp &&
+
+ git -C backfill-fp rev-list --quiet --objects --missing=print HEAD >missing &&
+ test_line_count = 52 missing &&
+
+ # --first-parent skips the side branch commits, so
+ # s/file.{1,2}.txt v1 blobs (only in side commit 1) are missed.
+ git -C backfill-fp backfill --first-parent HEAD &&
+
+ git -C backfill-fp rev-list --quiet --objects --missing=print HEAD >missing &&
+ test_line_count = 2 missing
+'
+
+test_expect_success 'backfill with --since' '
+ test_when_finished rm -rf backfill-since &&
+ git clone --no-checkout --filter=blob:none \
+ --single-branch --branch=main \
+ "file://$(pwd)/srv-revs.bare" backfill-since &&
+
+ git -C backfill-since rev-list --quiet --objects --missing=print HEAD >missing &&
+ test_line_count = 52 missing &&
+
+ # Use a cutoff between commits 4 and 5 (between v1 and v2
+ # iterations). Commits 5-8 still carry v1 of files 2-4 in
+ # their trees, but v1 of file.1.txt is only in commits 1-4.
+ SINCE=$(git -C backfill-since log --first-parent --reverse \
+ --format=%ct HEAD~1 | sed -n 5p) &&
+ git -C backfill-since backfill --since="@$((SINCE - 1))" HEAD &&
+
+ # 6 missing: v1 of file.1.txt in all 6 directories
+ git -C backfill-since rev-list --quiet --objects --missing=print HEAD >missing &&
+ test_line_count = 6 missing
+'
+
. "$TEST_DIRECTORY"/lib-httpd.sh
start_httpd
diff --git a/t/t8003-blame-corner-cases.sh b/t/t8003-blame-corner-cases.sh
index 731265541a..30e7960ace 100755
--- a/t/t8003-blame-corner-cases.sh
+++ b/t/t8003-blame-corner-cases.sh
@@ -49,80 +49,69 @@ test_expect_success setup '
'
test_expect_success 'straight copy without -C' '
-
- git blame uno | grep Second
-
+ git blame uno >actual &&
+ test_grep Second actual
'
test_expect_success 'straight move without -C' '
-
- git blame dos | grep Initial
-
+ git blame dos >actual &&
+ test_grep Initial actual
'
test_expect_success 'straight copy with -C' '
-
- git blame -C1 uno | grep Second
-
+ git blame -C1 uno >actual &&
+ test_grep Second actual
'
test_expect_success 'straight move with -C' '
-
- git blame -C1 dos | grep Initial
-
+ git blame -C1 dos >actual &&
+ test_grep Initial actual
'
test_expect_success 'straight copy with -C -C' '
-
- git blame -C -C1 uno | grep Initial
-
+ git blame -C -C1 uno >actual &&
+ test_grep Initial actual
'
test_expect_success 'straight move with -C -C' '
-
- git blame -C -C1 dos | grep Initial
-
+ git blame -C -C1 dos >actual &&
+ test_grep Initial actual
'
test_expect_success 'append without -C' '
-
- git blame -L2 tres | grep Second
-
+ git blame -L2 tres >actual &&
+ test_grep Second actual
'
test_expect_success 'append with -C' '
-
- git blame -L2 -C1 tres | grep Second
-
+ git blame -L2 -C1 tres >actual &&
+ test_grep Second actual
'
test_expect_success 'append with -C -C' '
-
- git blame -L2 -C -C1 tres | grep Second
-
+ git blame -L2 -C -C1 tres >actual &&
+ test_grep Second actual
'
test_expect_success 'append with -C -C -C' '
-
- git blame -L2 -C -C -C1 tres | grep Initial
-
+ git blame -L2 -C -C -C1 tres >actual &&
+ test_grep Initial actual
'
test_expect_success 'blame wholesale copy' '
-
- git blame -f -C -C1 HEAD^ -- cow | sed -e "$pick_fc" >current &&
+ git blame -f -C -C1 HEAD^ -- cow >actual &&
+ sed -e "$pick_fc" actual >current &&
cat >expected <<-\EOF &&
mouse-Initial
mouse-Second
mouse-Third
EOF
test_cmp expected current
-
'
test_expect_success 'blame wholesale copy and more' '
-
- git blame -f -C -C1 HEAD -- cow | sed -e "$pick_fc" >current &&
+ git blame -f -C -C1 HEAD -- cow >actual &&
+ sed -e "$pick_fc" actual >current &&
cat >expected <<-\EOF &&
mouse-Initial
mouse-Second
@@ -130,11 +119,9 @@ test_expect_success 'blame wholesale copy and more' '
mouse-Third
EOF
test_cmp expected current
-
'
test_expect_success 'blame wholesale copy and more in the index' '
-
cat >horse <<-\EOF &&
ABC
DEF
@@ -144,7 +131,8 @@ test_expect_success 'blame wholesale copy and more in the index' '
EOF
git add horse &&
test_when_finished "git rm -f horse" &&
- git blame -f -C -C1 -- horse | sed -e "$pick_fc" >current &&
+ git blame -f -C -C1 -- horse >actual &&
+ sed -e "$pick_fc" actual >current &&
cat >expected <<-\EOF &&
mouse-Initial
mouse-Second
@@ -153,11 +141,9 @@ test_expect_success 'blame wholesale copy and more in the index' '
mouse-Third
EOF
test_cmp expected current
-
'
test_expect_success 'blame during cherry-pick with file rename conflict' '
-
test_when_finished "git reset --hard && git checkout main" &&
git checkout HEAD~3 &&
echo MOUSE >> mouse &&
@@ -168,7 +154,8 @@ test_expect_success 'blame during cherry-pick with file rename conflict' '
(git cherry-pick HEAD@{1} || test $? -eq 1) &&
git show HEAD@{1}:rodent > rodent &&
git add rodent &&
- git blame -f -C -C1 rodent | sed -e "$pick_fc" >current &&
+ git blame -f -C -C1 rodent >actual &&
+ sed -e "$pick_fc" actual >current &&
cat >expected <<-\EOF &&
mouse-Initial
mouse-Second
@@ -246,14 +233,14 @@ test_expect_success 'setup file with CRLF newlines' '
test_expect_success 'blame file with CRLF core.autocrlf true' '
git config core.autocrlf true &&
git blame crlffile >actual &&
- grep "A U Thor" actual
+ test_grep "A U Thor" actual
'
test_expect_success 'blame file with CRLF attributes text' '
git config core.autocrlf false &&
echo "crlffile text" >.gitattributes &&
git blame crlffile >actual &&
- grep "A U Thor" actual
+ test_grep "A U Thor" actual
'
test_expect_success 'blame file with CRLF core.autocrlf=true' '
@@ -267,7 +254,7 @@ test_expect_success 'blame file with CRLF core.autocrlf=true' '
git checkout crlfinrepo &&
rm tmp &&
git blame crlfinrepo >actual &&
- grep "A U Thor" actual
+ test_grep "A U Thor" actual
'
test_expect_success 'setup coalesce tests' '
diff --git a/transport.c b/transport.c
index cb1befba8c..e53936d87b 100644
--- a/transport.c
+++ b/transport.c
@@ -1360,7 +1360,8 @@ static int pre_push_hook_feed_stdin(int hook_stdin_fd, void *pp_cb UNUSED, void
static void *pre_push_hook_data_alloc(void *feed_pipe_ctx)
{
- struct feed_pre_push_hook_data *data = xmalloc(sizeof(*data));
+ struct feed_pre_push_hook_data *data;
+ CALLOC_ARRAY(data, 1);
strbuf_init(&data->buf, 0);
data->refs = (struct ref *)feed_pipe_ctx;
return data;
diff --git a/worktree.c b/worktree.c
index 56732f8f33..d874e23b4e 100644
--- a/worktree.c
+++ b/worktree.c
@@ -58,7 +58,7 @@ static void add_head_info(struct worktree *wt)
static int is_current_worktree(struct worktree *wt)
{
- char *git_dir = absolute_pathdup(repo_get_git_dir(the_repository));
+ char *git_dir = absolute_pathdup(repo_get_git_dir(wt->repo));
char *wt_git_dir = get_worktree_git_dir(wt);
int is_current = !fspathcmp(git_dir, absolute_path(wt_git_dir));
free(wt_git_dir);
@@ -78,7 +78,7 @@ struct worktree *get_worktree_from_repository(struct repository *repo)
wt->is_bare = !repo->worktree;
if (fspathcmp(gitdir, commondir))
wt->id = xstrdup(find_last_dir_sep(gitdir) + 1);
- wt->is_current = is_current_worktree(wt);
+ wt->is_current = true;
add_head_info(wt);
free(gitdir);
@@ -227,11 +227,11 @@ struct worktree **get_worktrees_without_reading_head(void)
char *get_worktree_git_dir(const struct worktree *wt)
{
if (!wt)
- return xstrdup(repo_get_git_dir(the_repository));
+ BUG("%s() called with NULL worktree", __func__);
else if (!wt->id)
- return xstrdup(repo_get_common_dir(the_repository));
+ return xstrdup(repo_get_common_dir(wt->repo));
else
- return repo_common_path(the_repository, "worktrees/%s", wt->id);
+ return repo_common_path(wt->repo, "worktrees/%s", wt->id);
}
static struct worktree *find_worktree_by_suffix(struct worktree **list,
diff --git a/worktree.h b/worktree.h
index 026ef303e8..d19ec29dbb 100644
--- a/worktree.h
+++ b/worktree.h
@@ -16,7 +16,7 @@ struct worktree {
struct object_id head_oid;
int is_detached;
int is_bare;
- int is_current;
+ int is_current; /* does `path` match `repo->worktree` */
int lock_reason_valid; /* private */
int prune_reason_valid; /* private */
};
@@ -51,7 +51,6 @@ int submodule_uses_worktrees(const char *path);
/*
* Return git dir of the worktree. Note that the path may be relative.
- * If wt is NULL, git dir of current worktree is returned.
*/
char *get_worktree_git_dir(const struct worktree *wt);