summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Documentation/RelNotes/2.35.0.txt47
-rw-r--r--Documentation/config/gpg.txt5
-rw-r--r--Documentation/config/grep.txt3
-rw-r--r--Documentation/config/user.txt17
-rw-r--r--Documentation/git-apply.txt11
-rw-r--r--Makefile10
-rw-r--r--apply.c10
-rw-r--r--apply.h1
-rw-r--r--branch.c45
-rw-r--r--builtin/blame.c3
-rw-r--r--builtin/branch.c7
-rw-r--r--builtin/diff.c5
-rw-r--r--builtin/fetch.c129
-rw-r--r--builtin/log.c23
-rw-r--r--builtin/name-rev.c17
-rw-r--r--builtin/notes.c6
-rw-r--r--builtin/receive-pack.c98
-rw-r--r--commit-graph.c5
-rw-r--r--commit.c6
-rw-r--r--common-main.c9
-rw-r--r--config.mak.dev5
-rw-r--r--contrib/scalar/.gitignore2
-rw-r--r--contrib/scalar/Makefile45
-rw-r--r--contrib/scalar/README.md82
-rw-r--r--contrib/scalar/scalar.c826
-rw-r--r--contrib/scalar/scalar.txt145
-rw-r--r--contrib/scalar/t/Makefile78
-rwxr-xr-xcontrib/scalar/t/t9099-scalar.sh88
-rw-r--r--fmt-merge-msg.c5
-rw-r--r--git.c39
-rw-r--r--gpg-interface.c126
-rw-r--r--gpg-interface.h15
-rw-r--r--log-tree.c10
-rw-r--r--repo-settings.c3
-rw-r--r--revision.c11
-rw-r--r--sequencer.c9
-rw-r--r--t/Makefile10
-rw-r--r--t/chainlint.sed124
-rw-r--r--t/chainlint/arithmetic-expansion.expect6
-rw-r--r--t/chainlint/bash-array.expect4
-rw-r--r--t/chainlint/blank-line.expect2
-rw-r--r--t/chainlint/blank-line.test2
-rw-r--r--t/chainlint/block-comment.expect6
-rw-r--r--t/chainlint/block-comment.test8
-rw-r--r--t/chainlint/block.expect4
-rw-r--r--t/chainlint/block.test3
-rw-r--r--t/chainlint/broken-chain.expect4
-rw-r--r--t/chainlint/broken-chain.test2
-rw-r--r--t/chainlint/case-comment.expect8
-rw-r--r--t/chainlint/case-comment.test11
-rw-r--r--t/chainlint/case.expect10
-rw-r--r--t/chainlint/case.test6
-rw-r--r--t/chainlint/close-nested-and-parent-together.expect5
-rw-r--r--t/chainlint/close-subshell.expect16
-rw-r--r--t/chainlint/command-substitution.expect6
-rw-r--r--t/chainlint/comment.expect2
-rw-r--r--t/chainlint/complex-if-in-cuddled-loop.expect5
-rw-r--r--t/chainlint/complex-if-in-cuddled-loop.test2
-rw-r--r--t/chainlint/cuddled-if-then-else.expect5
-rw-r--r--t/chainlint/cuddled-if-then-else.test2
-rw-r--r--t/chainlint/cuddled-loop.expect5
-rw-r--r--t/chainlint/cuddled-loop.test2
-rw-r--r--t/chainlint/cuddled.expect22
-rw-r--r--t/chainlint/cuddled.test3
-rw-r--r--t/chainlint/exit-loop.expect6
-rw-r--r--t/chainlint/exit-subshell.expect2
-rw-r--r--t/chainlint/for-loop.expect8
-rw-r--r--t/chainlint/for-loop.test8
-rw-r--r--t/chainlint/here-doc-close-subshell.expect2
-rw-r--r--t/chainlint/here-doc-multi-line-command-subst.expect6
-rw-r--r--t/chainlint/here-doc-multi-line-string.expect4
-rw-r--r--t/chainlint/here-doc.expect10
-rw-r--r--t/chainlint/here-doc.test7
-rw-r--r--t/chainlint/if-in-loop.expect8
-rw-r--r--t/chainlint/if-in-loop.test6
-rw-r--r--t/chainlint/if-then-else.expect15
-rw-r--r--t/chainlint/if-then-else.test17
-rw-r--r--t/chainlint/incomplete-line.expect2
-rw-r--r--t/chainlint/inline-comment.expect9
-rw-r--r--t/chainlint/loop-in-if.expect8
-rw-r--r--t/chainlint/loop-in-if.test6
-rw-r--r--t/chainlint/multi-line-nested-command-substitution.expect10
-rw-r--r--t/chainlint/multi-line-string.expect12
-rw-r--r--t/chainlint/multi-line-string.test16
-rw-r--r--t/chainlint/negated-one-liner.expect4
-rw-r--r--t/chainlint/nested-cuddled-subshell.expect14
-rw-r--r--t/chainlint/nested-here-doc.expect8
-rw-r--r--t/chainlint/nested-subshell-comment.expect6
-rw-r--r--t/chainlint/nested-subshell-comment.test2
-rw-r--r--t/chainlint/nested-subshell.expect6
-rw-r--r--t/chainlint/nested-subshell.test1
-rw-r--r--t/chainlint/not-heredoc.expect14
-rw-r--r--t/chainlint/not-heredoc.test16
-rw-r--r--t/chainlint/one-liner.expect6
-rw-r--r--t/chainlint/one-liner.test4
-rw-r--r--t/chainlint/p4-filespec.expect2
-rw-r--r--t/chainlint/pipe.expect4
-rw-r--r--t/chainlint/pipe.test2
-rw-r--r--t/chainlint/semicolon.expect27
-rw-r--r--t/chainlint/semicolon.test4
-rw-r--r--t/chainlint/subshell-here-doc.expect15
-rw-r--r--t/chainlint/subshell-here-doc.test8
-rw-r--r--t/chainlint/subshell-one-liner.expect12
-rw-r--r--t/chainlint/t7900-subtree.expect10
-rw-r--r--t/chainlint/t7900-subtree.test4
-rw-r--r--t/chainlint/while-loop.expect8
-rw-r--r--t/chainlint/while-loop.test8
-rw-r--r--t/helper/test-trace2.c5
-rw-r--r--t/lib-gpg.sh65
-rwxr-xr-xt/perf/p2000-sparse-operations.sh4
-rwxr-xr-xt/t1092-sparse-checkout-compatibility.sh109
-rwxr-xr-xt/t2018-checkout-branch.sh2
-rwxr-xr-xt/t3200-branch.sh11
-rwxr-xr-xt/t3409-rebase-environ.sh23
-rwxr-xr-xt/t4126-apply-empty.sh22
-rwxr-xr-xt/t4202-log.sh80
-rwxr-xr-xt/t5504-fetch-receive-strict.sh2
-rwxr-xr-xt/t5516-fetch-push.sh32
-rwxr-xr-xt/t5553-set-upstream.sh22
-rwxr-xr-xt/t6200-fmt-merge-msg.sh68
-rwxr-xr-xt/t7031-verify-tag-signed-ssh.sh42
-rwxr-xr-xt/t7510-signed-commit.sh2
-rwxr-xr-xt/t7528-signed-commit-ssh.sh68
-rw-r--r--tag.c5
-rw-r--r--worktree.c8
-rw-r--r--worktree.h5
-rw-r--r--xdiff/xdiffi.c23
-rw-r--r--xdiff/xhistogram.c13
-rw-r--r--xdiff/xprepare.c35
129 files changed, 2622 insertions, 602 deletions
diff --git a/Documentation/RelNotes/2.35.0.txt b/Documentation/RelNotes/2.35.0.txt
index b1d8145dd9..dc72843e16 100644
--- a/Documentation/RelNotes/2.35.0.txt
+++ b/Documentation/RelNotes/2.35.0.txt
@@ -44,6 +44,24 @@ UI, Workflows & Features
* "default" and "reset" have been added to our color palette.
+ * The cryptographic signing using ssh keys can specify literal keys
+ for keytypes whose name do not begin with the "ssh-" prefix by
+ using the "key::" prefix mechanism (e.g. "key::ecdsa-sha2-nistp256").
+
+ * "git fetch" without the "--update-head-ok" option ought to protect
+ a checked out branch from getting updated, to prevent the working
+ tree that checks it out to go out of sync. The code was written
+ before the use of "git worktree" got widespread, and only checked
+ the branch that was checked out in the current worktree, which has
+ been updated.
+
+ * "git name-rev" has been tweaked to give output that is shorter and
+ easier to understand.
+
+ * "git apply" has been taught to ignore a message without a patch
+ with the "--allow-empty" option. It also learned to honor the
+ "--quiet" option given from the command line.
+
Performance, Internal Implementation, Development Support etc.
@@ -56,7 +74,6 @@ Performance, Internal Implementation, Development Support etc.
* The command line complation for "git send-email" options have been
tweaked to make it easier to keep it in sync with the command itself.
-
* Ensure that the sparseness of the in-core index matches the
index.sparse configuration specified by the repository immediately
after the on-disk index file is read.
@@ -101,6 +118,18 @@ Performance, Internal Implementation, Development Support etc.
nonexistent object name to refs to simulate error situations we
want to test Git in.
+ * "diff --histogram" optimization.
+
+ * Weather balloon to find compilers that do not grok variable
+ declaration in the for() loop.
+
+ * diff and blame commands have been taught to work better with sparse
+ index.
+
+ * The chainlint test script linter in the test suite has been updated.
+
+ * The DEVELOPER=yes build uses -std=gnu99 now.
+
Fixes since v2.34
-----------------
@@ -210,6 +239,21 @@ Fixes since v2.34
sparse checkout patterns.
(merge 8c5de0d265 ds/sparse-deep-pattern-checkout-fix later to maint).
+ * "git rebase -x" by mistake started exporting the GIT_DIR and
+ GIT_WORK_TREE environment variables when the command was rewritten
+ in C, which has been corrected.
+ (merge 434e0636db en/rebase-x-wo-git-dir-env later to maint).
+
+ * When "git log" implicitly enabled the "decoration" processing
+ without being explicitly asked with "--decorate" option, it failed
+ to read and honor the settings given by the "--decorate-refs"
+ option.
+
+ * "git fetch --set-upstream" did not check if there is a current
+ branch, leading to a segfault when it is run on a detached HEAD,
+ which has been corrected.
+ (merge 17baeaf82d ab/fetch-set-upstream-while-detached later to maint).
+
* Other code cleanup, docfix, build fix, etc.
(merge 74db416c9c cw/protocol-v2-doc-fix later to maint).
(merge f9b2b6684d ja/doc-cleanup later to maint).
@@ -227,3 +271,4 @@ Fixes since v2.34
(merge 0bf0de6cc7 tb/pack-revindex-on-disk-cleanup later to maint).
(merge 2c68f577fc ew/cbtree-remove-unused-and-broken-cb-unlink later to maint).
(merge eafd6e7e55 ab/die-with-bug later to maint).
+ (merge 91028f7659 jc/grep-patterntype-default-doc later to maint).
diff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt
index 4f30c7dbdd..c9be554c73 100644
--- a/Documentation/config/gpg.txt
+++ b/Documentation/config/gpg.txt
@@ -64,6 +64,11 @@ A repository that only allows signed commits can store the file
in the repository itself using a path relative to the top-level of the working tree.
This way only committers with an already valid key can add or change keys in the keyring.
+
+Since OpensSSH 8.8 this file allows specifying a key lifetime using valid-after &
+valid-before options. Git will mark signatures as valid if the signing key was
+valid at the time of the signatures creation. This allows users to change a
+signing key without invalidating all previously made signatures.
++
Using a SSH CA key with the cert-authority option
(see ssh-keygen(1) "CERTIFICATES") is also valid.
diff --git a/Documentation/config/grep.txt b/Documentation/config/grep.txt
index 44abe45a7c..182edd813a 100644
--- a/Documentation/config/grep.txt
+++ b/Documentation/config/grep.txt
@@ -8,7 +8,8 @@ grep.patternType::
Set the default matching behavior. Using a value of 'basic', 'extended',
'fixed', or 'perl' will enable the `--basic-regexp`, `--extended-regexp`,
`--fixed-strings`, or `--perl-regexp` option accordingly, while the
- value 'default' will return to the default matching behavior.
+ value 'default' will use the `grep.extendedRegexp` option to choose
+ between 'basic' and 'extended'.
grep.extendedRegexp::
If set to true, enable `--extended-regexp` option by default. This
diff --git a/Documentation/config/user.txt b/Documentation/config/user.txt
index ad78dce9ec..ec9233b060 100644
--- a/Documentation/config/user.txt
+++ b/Documentation/config/user.txt
@@ -36,10 +36,13 @@ user.signingKey::
commit, you can override the default selection with this variable.
This option is passed unchanged to gpg's --local-user parameter,
so you may specify a key using any method that gpg supports.
- If gpg.format is set to "ssh" this can contain the literal ssh public
- key (e.g.: "ssh-rsa XXXXXX identifier") or a file which contains it and
- corresponds to the private key used for signing. The private key
- needs to be available via ssh-agent. Alternatively it can be set to
- a file containing a private key directly. If not set git will call
- gpg.ssh.defaultKeyCommand (e.g.: "ssh-add -L") and try to use the first
- key available.
+ If gpg.format is set to `ssh` this can contain the path to either
+ your private ssh key or the public key when ssh-agent is used.
+ Alternatively it can contain a public key prefixed with `key::`
+ directly (e.g.: "key::ssh-rsa XXXXXX identifier"). The private key
+ needs to be available via ssh-agent. If not set git will call
+ gpg.ssh.defaultKeyCommand (e.g.: "ssh-add -L") and try to use the
+ first key available. For backward compatibility, a raw key which
+ begins with "ssh-", such as "ssh-rsa XXXXXX identifier", is treated
+ as "key::ssh-rsa XXXXXX identifier", but this form is deprecated;
+ use the `key::` form instead.
diff --git a/Documentation/git-apply.txt b/Documentation/git-apply.txt
index aa1ae56a25..b6d77f4206 100644
--- a/Documentation/git-apply.txt
+++ b/Documentation/git-apply.txt
@@ -16,7 +16,7 @@ SYNOPSIS
[--ignore-space-change | --ignore-whitespace]
[--whitespace=(nowarn|warn|fix|error|error-all)]
[--exclude=<path>] [--include=<path>] [--directory=<root>]
- [--verbose] [--unsafe-paths] [<patch>...]
+ [--verbose | --quiet] [--unsafe-paths] [--allow-empty] [<patch>...]
DESCRIPTION
-----------
@@ -228,6 +228,11 @@ behavior:
current patch being applied will be printed. This option will cause
additional information to be reported.
+-q::
+--quiet::
+ Suppress stderr output. Messages about patch status and progress
+ will not be printed.
+
--recount::
Do not trust the line counts in the hunk headers, but infer them
by inspecting the patch (e.g. after editing the patch without
@@ -251,6 +256,10 @@ When `git apply` is used as a "better GNU patch", the user can pass
the `--unsafe-paths` option to override this safety check. This option
has no effect when `--index` or `--cached` is in use.
+--allow-empty::
+ Don't return error for patches containing no diff. This includes
+ empty patches and patches with commit text only.
+
CONFIGURATION
-------------
diff --git a/Makefile b/Makefile
index 9c00a793e4..75ed168adb 100644
--- a/Makefile
+++ b/Makefile
@@ -1206,6 +1206,7 @@ endif
# Set CFLAGS, LDFLAGS and other *FLAGS variables. These might be
# tweaked by config.* below as well as the command-line, both of
# which'll override these defaults.
+# Older versions of GCC may require adding "-std=gnu99" at the end.
CFLAGS = -g -O2 -Wall
LDFLAGS =
CC_LD_DYNPATH = -Wl,-rpath,
@@ -2491,6 +2492,11 @@ OBJECTS += $(REFTABLE_OBJS) $(REFTABLE_TEST_OBJS)
ifndef NO_CURL
OBJECTS += http.o http-walker.o remote-curl.o
endif
+
+SCALAR_SOURCES := contrib/scalar/scalar.c
+SCALAR_OBJECTS := $(SCALAR_SOURCES:c=o)
+OBJECTS += $(SCALAR_OBJECTS)
+
.PHONY: objects
objects: $(OBJECTS)
@@ -2624,6 +2630,10 @@ $(REMOTE_CURL_PRIMARY): remote-curl.o http.o http-walker.o GIT-LDFLAGS $(GITLIBS
$(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) \
$(CURL_LIBCURL) $(EXPAT_LIBEXPAT) $(LIBS)
+contrib/scalar/scalar$X: $(SCALAR_OBJECTS) GIT-LDFLAGS $(GITLIBS)
+ $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) \
+ $(filter %.o,$^) $(LIBS)
+
$(LIB_FILE): $(LIB_OBJS)
$(QUIET_AR)$(RM) $@ && $(AR) $(ARFLAGS) $@ $^
diff --git a/apply.c b/apply.c
index 43a0aebf4e..fed195250b 100644
--- a/apply.c
+++ b/apply.c
@@ -4752,8 +4752,10 @@ static int apply_patch(struct apply_state *state,
}
if (!list && !skipped_patch) {
- error(_("unrecognized input"));
- res = -128;
+ if (!state->allow_empty) {
+ error(_("No valid patches in input (allow with \"--allow-empty\")"));
+ res = -128;
+ }
goto end;
}
@@ -5071,7 +5073,7 @@ int apply_parse_options(int argc, const char **argv,
N_("leave the rejected hunks in corresponding *.rej files")),
OPT_BOOL(0, "allow-overlap", &state->allow_overlap,
N_("allow overlapping hunks")),
- OPT__VERBOSE(&state->apply_verbosity, N_("be verbose")),
+ OPT__VERBOSITY(&state->apply_verbosity),
OPT_BIT(0, "inaccurate-eof", options,
N_("tolerate incorrectly detected missing new-line at the end of file"),
APPLY_OPT_INACCURATE_EOF),
@@ -5081,6 +5083,8 @@ int apply_parse_options(int argc, const char **argv,
OPT_CALLBACK(0, "directory", state, N_("root"),
N_("prepend <root> to all filenames"),
apply_option_parse_directory),
+ OPT_BOOL(0, "allow-empty", &state->allow_empty,
+ N_("don't return error for empty patches")),
OPT_END()
};
diff --git a/apply.h b/apply.h
index da3d95fa50..16202da160 100644
--- a/apply.h
+++ b/apply.h
@@ -66,6 +66,7 @@ struct apply_state {
int threeway;
int unidiff_zero;
int unsafe_paths;
+ int allow_empty;
/* Other non boolean parameters */
struct repository *repo;
diff --git a/branch.c b/branch.c
index 07a46430b3..2cfe496d24 100644
--- a/branch.c
+++ b/branch.c
@@ -64,7 +64,7 @@ int install_branch_config(int flag, const char *local, const char *origin, const
if (skip_prefix(remote, "refs/heads/", &shortname)
&& !strcmp(local, shortname)
&& !origin) {
- warning(_("Not setting branch %s as its own upstream."),
+ warning(_("not setting branch %s as its own upstream"),
local);
return 0;
}
@@ -116,7 +116,7 @@ int install_branch_config(int flag, const char *local, const char *origin, const
out_err:
strbuf_release(&key);
- error(_("Unable to write upstream branch configuration"));
+ error(_("unable to write upstream branch configuration"));
advise(_(tracking_advice),
origin ? origin : "",
@@ -153,7 +153,7 @@ static void setup_tracking(const char *new_ref, const char *orig_ref,
}
if (tracking.matches > 1)
- die(_("Not tracking: ambiguous information for ref %s"),
+ die(_("not tracking: ambiguous information for ref %s"),
orig_ref);
if (install_branch_config(config_flags, new_ref, tracking.remote,
@@ -186,7 +186,7 @@ int read_branch_desc(struct strbuf *buf, const char *branch_name)
int validate_branchname(const char *name, struct strbuf *ref)
{
if (strbuf_check_branch_ref(ref, name))
- die(_("'%s' is not a valid branch name."), name);
+ die(_("'%s' is not a valid branch name"), name);
return ref_exists(ref->buf);
}
@@ -199,18 +199,23 @@ int validate_branchname(const char *name, struct strbuf *ref)
*/
int validate_new_branchname(const char *name, struct strbuf *ref, int force)
{
- const char *head;
+ struct worktree **worktrees;
+ const struct worktree *wt;
if (!validate_branchname(name, ref))
return 0;
if (!force)
- die(_("A branch named '%s' already exists."),
+ die(_("a branch named '%s' already exists"),
ref->buf + strlen("refs/heads/"));
- head = resolve_ref_unsafe("HEAD", 0, NULL, NULL);
- if (!is_bare_repository() && head && !strcmp(head, ref->buf))
- die(_("Cannot force update the current branch."));
+ worktrees = get_worktrees();
+ wt = find_shared_symref(worktrees, "HEAD", ref->buf);
+ if (wt && !wt->is_bare)
+ die(_("cannot force update the branch '%s'"
+ "checked out at '%s'"),
+ ref->buf + strlen("refs/heads/"), wt->path);
+ free_worktrees(worktrees);
return 1;
}
@@ -230,7 +235,7 @@ static int validate_remote_tracking_branch(char *ref)
}
static const char upstream_not_branch[] =
-N_("Cannot setup tracking information; starting point '%s' is not a branch.");
+N_("cannot set up tracking information; starting point '%s' is not a branch");
static const char upstream_missing[] =
N_("the requested upstream branch '%s' does not exist");
static const char upstream_advice[] =
@@ -278,7 +283,7 @@ void create_branch(struct repository *r,
}
die(_(upstream_missing), start_name);
}
- die(_("Not a valid object name: '%s'."), start_name);
+ die(_("not a valid object name: '%s'"), start_name);
}
switch (dwim_ref(start_name, strlen(start_name), &oid, &real_ref, 0)) {
@@ -298,12 +303,12 @@ void create_branch(struct repository *r,
}
break;
default:
- die(_("Ambiguous object name: '%s'."), start_name);
+ die(_("ambiguous object name: '%s'"), start_name);
break;
}
if ((commit = lookup_commit_reference(r, &oid)) == NULL)
- die(_("Not a valid branch point: '%s'."), start_name);
+ die(_("not a valid branch point: '%s'"), start_name);
oidcpy(&oid, &commit->object.oid);
if (reflog)
@@ -357,14 +362,16 @@ void remove_branch_state(struct repository *r, int verbose)
void die_if_checked_out(const char *branch, int ignore_current_worktree)
{
+ struct worktree **worktrees = get_worktrees();
const struct worktree *wt;
- wt = find_shared_symref("HEAD", branch);
- if (!wt || (ignore_current_worktree && wt->is_current))
- return;
- skip_prefix(branch, "refs/heads/", &branch);
- die(_("'%s' is already checked out at '%s'"),
- branch, wt->path);
+ wt = find_shared_symref(worktrees, "HEAD", branch);
+ if (wt && (!ignore_current_worktree || !wt->is_current)) {
+ skip_prefix(branch, "refs/heads/", &branch);
+ die(_("'%s' is already checked out at '%s'"), branch, wt->path);
+ }
+
+ free_worktrees(worktrees);
}
int replace_each_worktree_head_symref(const char *oldref, const char *newref,
diff --git a/builtin/blame.c b/builtin/blame.c
index f9ee3f8c68..7fafeac408 100644
--- a/builtin/blame.c
+++ b/builtin/blame.c
@@ -939,6 +939,9 @@ parse_done:
revs.diffopt.flags.follow_renames = 0;
argc = parse_options_end(&ctx);
+ prepare_repo_settings(the_repository);
+ the_repository->settings.command_requires_full_index = 0;
+
if (incremental || (output_option & OUTPUT_PORCELAIN)) {
if (show_progress > 0)
die(_("--progress can't be used with --incremental or porcelain formats"));
diff --git a/builtin/branch.c b/builtin/branch.c
index 81b5c111cb..6c8b0fcc11 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -192,6 +192,7 @@ static void delete_branch_config(const char *branchname)
static int delete_branches(int argc, const char **argv, int force, int kinds,
int quiet)
{
+ struct worktree **worktrees;
struct commit *head_rev = NULL;
struct object_id oid;
char *name = NULL;
@@ -228,6 +229,9 @@ static int delete_branches(int argc, const char **argv, int force, int kinds,
if (!head_rev)
die(_("Couldn't look up commit object for HEAD"));
}
+
+ worktrees = get_worktrees();
+
for (i = 0; i < argc; i++, strbuf_reset(&bname)) {
char *target = NULL;
int flags = 0;
@@ -238,7 +242,7 @@ static int delete_branches(int argc, const char **argv, int force, int kinds,
if (kinds == FILTER_REFS_BRANCHES) {
const struct worktree *wt =
- find_shared_symref("HEAD", name);
+ find_shared_symref(worktrees, "HEAD", name);
if (wt) {
error(_("Cannot delete branch '%s' "
"checked out at '%s'"),
@@ -299,6 +303,7 @@ static int delete_branches(int argc, const char **argv, int force, int kinds,
free(name);
strbuf_release(&bname);
+ free_worktrees(worktrees);
return ret;
}
diff --git a/builtin/diff.c b/builtin/diff.c
index dd8ce688ba..fa4683377e 100644
--- a/builtin/diff.c
+++ b/builtin/diff.c
@@ -437,6 +437,11 @@ int cmd_diff(int argc, const char **argv, const char *prefix)
prefix = setup_git_directory_gently(&nongit);
+ if (!nongit) {
+ prepare_repo_settings(the_repository);
+ the_repository->settings.command_requires_full_index = 0;
+ }
+
if (!no_index) {
/*
* Treat git diff with at least one path outside of the
diff --git a/builtin/fetch.c b/builtin/fetch.c
index f7abbc31ff..f1fe73a3e0 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -28,6 +28,7 @@
#include "promisor-remote.h"
#include "commit-graph.h"
#include "shallow.h"
+#include "worktree.h"
#define FORCED_UPDATES_DELAY_WARNING_IN_MS (10 * 1000)
@@ -552,7 +553,7 @@ static struct ref *get_ref_map(struct remote *remote,
for (i = 0; i < fetch_refspec->nr; i++)
get_fetch_map(ref_map, &fetch_refspec->items[i], &oref_tail, 1);
} else if (refmap.nr) {
- die("--refmap option is only meaningful with command-line refspec(s).");
+ die("--refmap option is only meaningful with command-line refspec(s)");
} else {
/* Use the defaults */
struct branch *branch = branch_get(NULL);
@@ -583,7 +584,7 @@ static struct ref *get_ref_map(struct remote *remote,
} else if (!prefetch) {
ref_map = get_remote_ref(remote_refs, "HEAD");
if (!ref_map)
- die(_("Couldn't find remote ref HEAD"));
+ die(_("couldn't find remote ref HEAD"));
ref_map->fetch_head_status = FETCH_HEAD_MERGE;
tail = &ref_map->next;
}
@@ -848,13 +849,12 @@ static void format_display(struct strbuf *display, char code,
static int update_local_ref(struct ref *ref,
struct ref_transaction *transaction,
- const char *remote,
- const struct ref *remote_ref,
- struct strbuf *display,
- int summary_width)
+ const char *remote, const struct ref *remote_ref,
+ struct strbuf *display, int summary_width,
+ struct worktree **worktrees)
{
struct commit *current = NULL, *updated;
- struct branch *current_branch = branch_get(NULL);
+ const struct worktree *wt;
const char *pretty_ref = prettify_refname(ref->name);
int fast_forward = 0;
@@ -868,16 +868,17 @@ static int update_local_ref(struct ref *ref,
return 0;
}
- if (current_branch &&
- !strcmp(ref->name, current_branch->name) &&
- !(update_head_ok || is_bare_repository()) &&
- !is_null_oid(&ref->old_oid)) {
+ if (!update_head_ok &&
+ (wt = find_shared_symref(worktrees, "HEAD", ref->name)) &&
+ !wt->is_bare && !is_null_oid(&ref->old_oid)) {
/*
* If this is the head, and it's not okay to update
* the head, and the old value of the head isn't empty...
*/
format_display(display, '!', _("[rejected]"),
- _("can't fetch in current branch"),
+ wt->is_current ?
+ _("can't fetch in current branch") :
+ _("checked out in another worktree"),
remote, pretty_ref, summary_width);
return 1;
}
@@ -1067,16 +1068,17 @@ static void close_fetch_head(struct fetch_head *fetch_head)
}
static const char warn_show_forced_updates[] =
-N_("Fetch normally indicates which branches had a forced update,\n"
- "but that check has been disabled. To re-enable, use '--show-forced-updates'\n"
- "flag or run 'git config fetch.showForcedUpdates true'.");
+N_("fetch normally indicates which branches had a forced update,\n"
+ "but that check has been disabled; to re-enable, use '--show-forced-updates'\n"
+ "flag or run 'git config fetch.showForcedUpdates true'");
static const char warn_time_show_forced_updates[] =
-N_("It took %.2f seconds to check forced updates. You can use\n"
+N_("it took %.2f seconds to check forced updates; you can use\n"
"'--no-show-forced-updates' or run 'git config fetch.showForcedUpdates false'\n"
- " to avoid this check.\n");
+ "to avoid this check\n");
static int store_updated_refs(const char *raw_url, const char *remote_name,
- int connectivity_checked, struct ref *ref_map)
+ int connectivity_checked, struct ref *ref_map,
+ struct worktree **worktrees)
{
struct fetch_head fetch_head;
int url_len, i, rc = 0;
@@ -1205,7 +1207,8 @@ static int store_updated_refs(const char *raw_url, const char *remote_name,
strbuf_reset(&note);
if (ref) {
rc |= update_local_ref(ref, transaction, what,
- rm, &note, summary_width);
+ rm, &note, summary_width,
+ worktrees);
free(ref);
} else if (write_fetch_head || dry_run) {
/*
@@ -1298,7 +1301,9 @@ static int check_exist_and_connected(struct ref *ref_map)
return check_connected(iterate_ref_map, &rm, &opt);
}
-static int fetch_and_consume_refs(struct transport *transport, struct ref *ref_map)
+static int fetch_and_consume_refs(struct transport *transport,
+ struct ref *ref_map,
+ struct worktree **worktrees)
{
int connectivity_checked = 1;
int ret;
@@ -1319,10 +1324,8 @@ static int fetch_and_consume_refs(struct transport *transport, struct ref *ref_m
}
trace2_region_enter("fetch", "consume_refs", the_repository);
- ret = store_updated_refs(transport->url,
- transport->remote->name,
- connectivity_checked,
- ref_map);
+ ret = store_updated_refs(transport->url, transport->remote->name,
+ connectivity_checked, ref_map, worktrees);
trace2_region_leave("fetch", "consume_refs", the_repository);
out:
@@ -1385,18 +1388,18 @@ static int prune_refs(struct refspec *rs, struct ref *ref_map,
return result;
}
-static void check_not_current_branch(struct ref *ref_map)
+static void check_not_current_branch(struct ref *ref_map,
+ struct worktree **worktrees)
{
- struct branch *current_branch = branch_get(NULL);
-
- if (is_bare_repository() || !current_branch)
- return;
-
+ const struct worktree *wt;
for (; ref_map; ref_map = ref_map->next)
- if (ref_map->peer_ref && !strcmp(current_branch->refname,
- ref_map->peer_ref->name))
- die(_("Refusing to fetch into current branch %s "
- "of non-bare repository"), current_branch->refname);
+ if (ref_map->peer_ref &&
+ (wt = find_shared_symref(worktrees, "HEAD",
+ ref_map->peer_ref->name)) &&
+ !wt->is_bare)
+ die(_("refusing to fetch into branch '%s' "
+ "checked out at '%s'"),
+ ref_map->peer_ref->name, wt->path);
}
static int truncate_fetch_head(void)
@@ -1414,10 +1417,10 @@ static void set_option(struct transport *transport, const char *name, const char
{
int r = transport_set_option(transport, name, value);
if (r < 0)
- die(_("Option \"%s\" value \"%s\" is not valid for %s"),
+ die(_("option \"%s\" value \"%s\" is not valid for %s"),
name, value, transport->url);
if (r > 0)
- warning(_("Option \"%s\" is ignored for %s\n"),
+ warning(_("option \"%s\" is ignored for %s\n"),
name, transport->url);
}
@@ -1451,7 +1454,7 @@ static void add_negotiation_tips(struct git_transport_options *smart_options)
old_nr = oids->nr;
for_each_glob_ref(add_oid, s, oids);
if (old_nr == oids->nr)
- warning("Ignoring --negotiation-tip=%s because it does not match any refs",
+ warning("ignoring --negotiation-tip=%s because it does not match any refs",
s);
}
smart_options->negotiation_tips = oids;
@@ -1489,12 +1492,13 @@ static struct transport *prepare_transport(struct remote *remote, int deepen)
if (transport->smart_options)
add_negotiation_tips(transport->smart_options);
else
- warning("Ignoring --negotiation-tip because the protocol does not support it.");
+ warning("ignoring --negotiation-tip because the protocol does not support it");
}
return transport;
}
-static void backfill_tags(struct transport *transport, struct ref *ref_map)
+static void backfill_tags(struct transport *transport, struct ref *ref_map,
+ struct worktree **worktrees)
{
int cannot_reuse;
@@ -1515,7 +1519,7 @@ static void backfill_tags(struct transport *transport, struct ref *ref_map)
transport_set_option(transport, TRANS_OPT_FOLLOWTAGS, NULL);
transport_set_option(transport, TRANS_OPT_DEPTH, "0");
transport_set_option(transport, TRANS_OPT_DEEPEN_RELATIVE, NULL);
- fetch_and_consume_refs(transport, ref_map);
+ fetch_and_consume_refs(transport, ref_map, worktrees);
if (gsecondary) {
transport_disconnect(gsecondary);
@@ -1533,6 +1537,7 @@ static int do_fetch(struct transport *transport,
struct transport_ls_refs_options transport_ls_refs_options =
TRANSPORT_LS_REFS_OPTIONS_INIT;
int must_list_refs = 1;
+ struct worktree **worktrees = get_worktrees();
if (tags == TAGS_DEFAULT) {
if (transport->remote->fetch_tags == 2)
@@ -1588,7 +1593,7 @@ static int do_fetch(struct transport *transport,
ref_map = get_ref_map(transport->remote, remote_refs, rs,
tags, &autotags);
if (!update_head_ok)
- check_not_current_branch(ref_map);
+ check_not_current_branch(ref_map, worktrees);
if (tags == TAGS_DEFAULT && autotags)
transport_set_option(transport, TRANS_OPT_FOLLOWTAGS, "1");
@@ -1606,7 +1611,7 @@ static int do_fetch(struct transport *transport,
transport->url);
}
}
- if (fetch_and_consume_refs(transport, ref_map)) {
+ if (fetch_and_consume_refs(transport, ref_map, worktrees)) {
free_refs(ref_map);
retcode = 1;
goto cleanup;
@@ -1638,6 +1643,16 @@ static int do_fetch(struct transport *transport,
}
}
if (source_ref) {
+ if (!branch) {
+ const char *shortname = source_ref->name;
+ skip_prefix(shortname, "refs/heads/", &shortname);
+
+ warning(_("could not set upstream of HEAD to '%s' from '%s' when "
+ "it does not point to any branch."),
+ shortname, transport->remote->name);
+ goto skip;
+ }
+
if (!strcmp(source_ref->name, "HEAD") ||
starts_with(source_ref->name, "refs/heads/"))
install_branch_config(0,
@@ -1651,11 +1666,11 @@ static int do_fetch(struct transport *transport,
else
warning(_("unknown branch type"));
} else {
- warning(_("no source branch found.\n"
- "you need to specify exactly one branch with the --set-upstream option."));
+ warning(_("no source branch found;\n"
+ "you need to specify exactly one branch with the --set-upstream option"));
}
}
- skip:
+skip:
free_refs(ref_map);
/* if neither --no-tags nor --tags was specified, do automated tag
@@ -1665,11 +1680,12 @@ static int do_fetch(struct transport *transport,
ref_map = NULL;
find_non_local_tags(remote_refs, &ref_map, &tail);
if (ref_map)
- backfill_tags(transport, ref_map);
+ backfill_tags(transport, ref_map, worktrees);
free_refs(ref_map);
}
- cleanup:
+cleanup:
+ free_worktrees(worktrees);
return retcode;
}
@@ -1790,7 +1806,7 @@ static int fetch_failed_to_start(struct strbuf *out, void *cb, void *task_cb)
struct parallel_fetch_state *state = cb;
const char *remote = task_cb;
- state->result = error(_("Could not fetch %s"), remote);
+ state->result = error(_("could not fetch %s"), remote);
return 0;
}
@@ -1845,7 +1861,7 @@ static int fetch_multiple(struct string_list *list, int max_children)
if (verbosity >= 0)
printf(_("Fetching %s\n"), name);
if (run_command_v_opt(argv.v, RUN_GIT_CMD)) {
- error(_("Could not fetch %s"), name);
+ error(_("could not fetch %s"), name);
result = 1;
}
strvec_pop(&argv);
@@ -1906,8 +1922,8 @@ static int fetch_one(struct remote *remote, int argc, const char **argv,
int remote_via_config = remote_is_configured(remote, 0);
if (!remote)
- die(_("No remote repository specified. Please, specify either a URL or a\n"
- "remote name from which new revisions should be fetched."));
+ die(_("no remote repository specified; please specify either a URL or a\n"
+ "remote name from which new revisions should be fetched"));
gtransport = prepare_transport(remote, 1);
@@ -1942,7 +1958,7 @@ static int fetch_one(struct remote *remote, int argc, const char **argv,
if (!strcmp(argv[i], "tag")) {
i++;
if (i >= argc)
- die(_("You need to specify a tag name."));
+ die(_("you need to specify a tag name"));
refspec_appendf(&rs, "refs/tags/%s:refs/tags/%s",
argv[i], argv[i]);
@@ -2010,7 +2026,7 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)
if (deepen_relative) {
if (deepen_relative < 0)
- die(_("Negative depth in --deepen is not supported"));
+ die(_("negative depth in --deepen is not supported"));
if (depth)
die(_("--deepen and --depth are mutually exclusive"));
depth = xstrfmt("%d", deepen_relative);
@@ -2047,14 +2063,15 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)
/* All arguments are assumed to be remotes or groups */
for (i = 0; i < argc; i++)
if (!add_remote_or_group(argv[i], &list))
- die(_("No such remote or remote group: %s"), argv[i]);
+ die(_("no such remote or remote group: %s"),
+ argv[i]);
} else {
/* Single remote or group */
(void) add_remote_or_group(argv[0], &list);
if (list.nr > 1) {
/* More than one remote */
if (argc > 1)
- die(_("Fetching a group and specifying refspecs does not make sense"));
+ die(_("fetching a group and specifying refspecs does not make sense"));
} else {
/* Zero or one remotes */
remote = remote_get(argv[0]);
@@ -2075,7 +2092,7 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)
if (gtransport->smart_options) {
gtransport->smart_options->acked_commits = &acked_commits;
} else {
- warning(_("Protocol does not support --negotiate-only, exiting."));
+ warning(_("protocol does not support --negotiate-only, exiting"));
return 1;
}
if (server_options.nr)
diff --git a/builtin/log.c b/builtin/log.c
index a7bca8353b..942c07ece6 100644
--- a/builtin/log.c
+++ b/builtin/log.c
@@ -245,10 +245,24 @@ static void cmd_log_init_finish(int argc, const char **argv, const char *prefix,
rev->abbrev_commit = 0;
}
- if (rev->commit_format == CMIT_FMT_USERFORMAT && !w.decorate)
- decoration_style = 0;
+ if (rev->commit_format == CMIT_FMT_USERFORMAT) {
+ if (!w.decorate) {
+ /*
+ * Disable decoration loading if the format will not
+ * show them anyway.
+ */
+ decoration_style = 0;
+ } else if (!decoration_style) {
+ /*
+ * If we are going to show them, make sure we do load
+ * them here, but taking care not to override a
+ * specific style set by config or --decorate.
+ */
+ decoration_style = DECORATE_SHORT_REFS;
+ }
+ }
- if (decoration_style) {
+ if (decoration_style || rev->simplify_by_decoration) {
const struct string_list *config_exclude =
repo_config_get_value_multi(the_repository,
"log.excludeDecoration");
@@ -260,7 +274,8 @@ static void cmd_log_init_finish(int argc, const char **argv, const char *prefix,
item->string);
}
- rev->show_decorations = 1;
+ if (decoration_style)
+ rev->show_decorations = 1;
load_ref_decorations(&decoration_filter, decoration_style);
}
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index b221d30014..27f60153a6 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -44,11 +44,20 @@ static struct rev_name *get_commit_rev_name(const struct commit *commit)
return is_valid_rev_name(name) ? name : NULL;
}
+static int effective_distance(int distance, int generation)
+{
+ return distance + (generation > 0 ? MERGE_TRAVERSAL_WEIGHT : 0);
+}
+
static int is_better_name(struct rev_name *name,
timestamp_t taggerdate,
+ int generation,
int distance,
int from_tag)
{
+ int name_distance = effective_distance(name->distance, name->generation);
+ int new_distance = effective_distance(distance, generation);
+
/*
* When comparing names based on tags, prefer names
* based on the older tag, even if it is farther away.
@@ -56,7 +65,7 @@ static int is_better_name(struct rev_name *name,
if (from_tag && name->from_tag)
return (name->taggerdate > taggerdate ||
(name->taggerdate == taggerdate &&
- name->distance > distance));
+ name_distance > new_distance));
/*
* We know that at least one of them is a non-tag at this point.
@@ -69,8 +78,8 @@ static int is_better_name(struct rev_name *name,
* We are now looking at two non-tags. Tiebreak to favor
* shorter hops.
*/
- if (name->distance != distance)
- return name->distance > distance;
+ if (name_distance != new_distance)
+ return name_distance > new_distance;
/* ... or tiebreak to favor older date */
if (name->taggerdate != taggerdate)
@@ -88,7 +97,7 @@ static struct rev_name *create_or_update_name(struct commit *commit,
struct rev_name *name = commit_rev_name_at(&rev_names, commit);
if (is_valid_rev_name(name)) {
- if (!is_better_name(name, taggerdate, distance, from_tag))
+ if (!is_better_name(name, taggerdate, generation, distance, from_tag))
return NULL;
/*
diff --git a/builtin/notes.c b/builtin/notes.c
index 85d1abad88..118db9e455 100644
--- a/builtin/notes.c
+++ b/builtin/notes.c
@@ -860,15 +860,19 @@ static int merge(int argc, const char **argv, const char *prefix)
update_ref(msg.buf, default_notes_ref(), &result_oid, NULL, 0,
UPDATE_REFS_DIE_ON_ERR);
else { /* Merge has unresolved conflicts */
+ struct worktree **worktrees;
const struct worktree *wt;
/* Update .git/NOTES_MERGE_PARTIAL with partial merge result */
update_ref(msg.buf, "NOTES_MERGE_PARTIAL", &result_oid, NULL,
0, UPDATE_REFS_DIE_ON_ERR);
/* Store ref-to-be-updated into .git/NOTES_MERGE_REF */
- wt = find_shared_symref("NOTES_MERGE_REF", default_notes_ref());
+ worktrees = get_worktrees();
+ wt = find_shared_symref(worktrees, "NOTES_MERGE_REF",
+ default_notes_ref());
if (wt)
die(_("a notes merge into %s is already in-progress at %s"),
default_notes_ref(), wt->path);
+ free_worktrees(worktrees);
if (create_symref("NOTES_MERGE_REF", default_notes_ref(), NULL))
die(_("failed to store link to current notes ref (%s)"),
default_notes_ref());
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 313b372a11..4f92e6f059 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -175,7 +175,7 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
strbuf_addf(&fsck_msg_types, "%c%s=%s",
fsck_msg_types.len ? ',' : '=', var, value);
else
- warning("Skipping unknown msg id '%s'", var);
+ warning("skipping unknown msg id '%s'", var);
return 0;
}
@@ -769,8 +769,10 @@ static void prepare_push_cert_sha1(struct child_process *proc)
memset(&sigcheck, '\0', sizeof(sigcheck));
bogs = parse_signed_buffer(push_cert.buf, push_cert.len);
- check_signature(push_cert.buf, bogs, push_cert.buf + bogs,
- push_cert.len - bogs, &sigcheck);
+ sigcheck.payload = xmemdupz(push_cert.buf, bogs);
+ sigcheck.payload_len = bogs;
+ check_signature(&sigcheck, push_cert.buf + bogs,
+ push_cert.len - bogs);
nonce_status = check_nonce(push_cert.buf, bogs);
}
@@ -1432,29 +1434,22 @@ static const char *push_to_checkout(unsigned char *hash,
static const char *update_worktree(unsigned char *sha1, const struct worktree *worktree)
{
- const char *retval, *work_tree, *git_dir = NULL;
+ const char *retval, *git_dir;
struct strvec env = STRVEC_INIT;
- if (worktree && worktree->path)
- work_tree = worktree->path;
- else if (git_work_tree_cfg)
- work_tree = git_work_tree_cfg;
- else
- work_tree = "..";
+ if (!worktree || !worktree->path)
+ BUG("worktree->path must be non-NULL");
- if (is_bare_repository())
+ if (worktree->is_bare)
return "denyCurrentBranch = updateInstead needs a worktree";
- if (worktree)
- git_dir = get_worktree_git_dir(worktree);
- if (!git_dir)
- git_dir = get_git_dir();
+ git_dir = get_worktree_git_dir(worktree);
strvec_pushf(&env, "GIT_DIR=%s", absolute_path(git_dir));
if (!hook_exists(push_to_checkout_hook))
- retval = push_to_deploy(sha1, &env, work_tree);
+ retval = push_to_deploy(sha1, &env, worktree->path);
else
- retval = push_to_checkout(sha1, &env, work_tree);
+ retval = push_to_checkout(sha1, &env, worktree->path);
strvec_clear(&env);
return retval;
@@ -1469,19 +1464,22 @@ static const char *update(struct command *cmd, struct shallow_info *si)
struct object_id *old_oid = &cmd->old_oid;
struct object_id *new_oid = &cmd->new_oid;
int do_update_worktree = 0;
- const struct worktree *worktree = is_bare_repository() ? NULL : find_shared_symref("HEAD", name);
+ struct worktree **worktrees = get_worktrees();
+ const struct worktree *worktree =
+ find_shared_symref(worktrees, "HEAD", name);
/* only refs/... are allowed */
if (!starts_with(name, "refs/") || check_refname_format(name + 5, 0)) {
rp_error("refusing to create funny ref '%s' remotely", name);
- return "funny refname";
+ ret = "funny refname";
+ goto out;
}
strbuf_addf(&namespaced_name_buf, "%s%s", get_git_namespace(), name);
free(namespaced_name);
namespaced_name = strbuf_detach(&namespaced_name_buf, NULL);
- if (worktree) {
+ if (worktree && !worktree->is_bare) {
switch (deny_current_branch) {
case DENY_IGNORE:
break;
@@ -1493,7 +1491,8 @@ static const char *update(struct command *cmd, struct shallow_info *si)
rp_error("refusing to update checked out branch: %s", name);
if (deny_current_branch == DENY_UNCONFIGURED)
refuse_unconfigured_deny();
- return "branch is currently checked out";
+ ret = "branch is currently checked out";
+ goto out;
case DENY_UPDATE_INSTEAD:
/* pass -- let other checks intervene first */
do_update_worktree = 1;
@@ -1504,13 +1503,15 @@ static const char *update(struct command *cmd, struct shallow_info *si)
if (!is_null_oid(new_oid) && !has_object_file(new_oid)) {
error("unpack should have generated %s, "
"but I can't find it!", oid_to_hex(new_oid));
- return "bad pack";
+ ret = "bad pack";
+ goto out;
}
if (!is_null_oid(old_oid) && is_null_oid(new_oid)) {
if (deny_deletes && starts_with(name, "refs/heads/")) {
rp_error("denying ref deletion for %s", name);
- return "deletion prohibited";
+ ret = "deletion prohibited";
+ goto out;
}
if (worktree || (head_name && !strcmp(namespaced_name, head_name))) {
@@ -1526,9 +1527,11 @@ static const char *update(struct command *cmd, struct shallow_info *si)
if (deny_delete_current == DENY_UNCONFIGURED)
refuse_unconfigured_deny_delete_current();
rp_error("refusing to delete the current branch: %s", name);
- return "deletion of the current branch prohibited";
+ ret = "deletion of the current branch prohibited";
+ goto out;
default:
- return "Invalid denyDeleteCurrent setting";
+ ret = "Invalid denyDeleteCurrent setting";
+ goto out;
}
}
}
@@ -1546,25 +1549,28 @@ static const char *update(struct command *cmd, struct shallow_info *si)
old_object->type != OBJ_COMMIT ||
new_object->type != OBJ_COMMIT) {
error("bad sha1 objects for %s", name);
- return "bad ref";
+ ret = "bad ref";
+ goto out;
}
old_commit = (struct commit *)old_object;
new_commit = (struct commit *)new_object;
if (!in_merge_bases(old_commit, new_commit)) {
rp_error("denying non-fast-forward %s"
" (you should pull first)", name);
- return "non-fast-forward";
+ ret = "non-fast-forward";
+ goto out;
}
}
if (run_update_hook(cmd)) {
rp_error("hook declined to update %s", name);
- return "hook declined";
+ ret = "hook declined";
+ goto out;
}
if (do_update_worktree) {
- ret = update_worktree(new_oid->hash, find_shared_symref("HEAD", name));
+ ret = update_worktree(new_oid->hash, worktree);
if (ret)
- return ret;
+ goto out;
}
if (is_null_oid(new_oid)) {
@@ -1572,9 +1578,9 @@ static const char *update(struct command *cmd, struct shallow_info *si)
if (!parse_object(the_repository, old_oid)) {
old_oid = NULL;
if (ref_exists(name)) {
- rp_warning("Allowing deletion of corrupt ref.");
+ rp_warning("allowing deletion of corrupt ref");
} else {
- rp_warning("Deleting a non-existent ref.");
+ rp_warning("deleting a non-existent ref");
cmd->did_not_exist = 1;
}
}
@@ -1583,17 +1589,19 @@ static const char *update(struct command *cmd, struct shallow_info *si)
old_oid,
0, "push", &err)) {
rp_error("%s", err.buf);
- strbuf_release(&err);
- return "failed to delete";
+ ret = "failed to delete";
+ } else {
+ ret = NULL; /* good */
}
strbuf_release(&err);
- return NULL; /* good */
}
else {
struct strbuf err = STRBUF_INIT;
if (shallow_update && si->shallow_ref[cmd->index] &&
- update_shallow_ref(cmd, si))
- return "shallow error";
+ update_shallow_ref(cmd, si)) {
+ ret = "shallow error";
+ goto out;
+ }
if (ref_transaction_update(transaction,
namespaced_name,
@@ -1601,14 +1609,16 @@ static const char *update(struct command *cmd, struct shallow_info *si)
0, "push",
&err)) {
rp_error("%s", err.buf);
- strbuf_release(&err);
-
- return "failed to update ref";
+ ret = "failed to update ref";
+ } else {
+ ret = NULL; /* good */
}
strbuf_release(&err);
-
- return NULL; /* good */
}
+
+out:
+ free_worktrees(worktrees);
+ return ret;
}
static void run_update_post_hook(struct command *commands)
@@ -2474,9 +2484,9 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
argc = parse_options(argc, argv, prefix, options, receive_pack_usage, 0);
if (argc > 1)
- usage_msg_opt(_("Too many arguments."), receive_pack_usage, options);
+ usage_msg_opt(_("too many arguments"), receive_pack_usage, options);
if (argc == 0)
- usage_msg_opt(_("You must specify a directory."), receive_pack_usage, options);
+ usage_msg_opt(_("you must specify a directory"), receive_pack_usage, options);
service_dir = argv[0];
diff --git a/commit-graph.c b/commit-graph.c
index 2706683acf..265c010122 100644
--- a/commit-graph.c
+++ b/commit-graph.c
@@ -632,10 +632,13 @@ static int prepare_commit_graph(struct repository *r)
struct object_directory *odb;
/*
+ * Early return if there is no git dir or if the commit graph is
+ * disabled.
+ *
* This must come before the "already attempted?" check below, because
* we want to disable even an already-loaded graph file.
*/
- if (r->commit_graph_disabled)
+ if (!r->gitdir || r->commit_graph_disabled)
return 0;
if (r->objects->commit_graph_attempted)
diff --git a/commit.c b/commit.c
index 551de4903c..a348f085b2 100644
--- a/commit.c
+++ b/commit.c
@@ -1212,8 +1212,10 @@ int check_commit_signature(const struct commit *commit, struct signature_check *
if (parse_signed_commit(commit, &payload, &signature, the_hash_algo) <= 0)
goto out;
- ret = check_signature(payload.buf, payload.len, signature.buf,
- signature.len, sigc);
+
+ sigc->payload_type = SIGNATURE_PAYLOAD_COMMIT;
+ sigc->payload = strbuf_detach(&payload, &sigc->payload_len);
+ ret = check_signature(sigc, signature.buf, signature.len);
out:
strbuf_release(&payload);
diff --git a/common-main.c b/common-main.c
index 71e21dd20a..eafc70718a 100644
--- a/common-main.c
+++ b/common-main.c
@@ -51,7 +51,10 @@ int main(int argc, const char **argv)
result = cmd_main(argc, argv);
- trace2_cmd_exit(result);
-
- return result;
+ /*
+ * We define exit() to call trace2_cmd_exit_fl() in
+ * git-compat-util.h. Whether we reach this or exit()
+ * elsewhere we'll always run our trace2 exit handler.
+ */
+ exit(result);
}
diff --git a/config.mak.dev b/config.mak.dev
index 7673fed114..d4afac6b51 100644
--- a/config.mak.dev
+++ b/config.mak.dev
@@ -19,6 +19,11 @@ endif
endif
endif
endif
+
+ifneq ($(or $(filter gcc6,$(COMPILER_FEATURES)),$(filter clang7,$(COMPILER_FEATURES))),)
+DEVELOPER_CFLAGS += -std=gnu99
+endif
+
DEVELOPER_CFLAGS += -Wdeclaration-after-statement
DEVELOPER_CFLAGS += -Wformat-security
DEVELOPER_CFLAGS += -Wold-style-definition
diff --git a/contrib/scalar/.gitignore b/contrib/scalar/.gitignore
new file mode 100644
index 0000000000..ff3d47e84d
--- /dev/null
+++ b/contrib/scalar/.gitignore
@@ -0,0 +1,2 @@
+/*.exe
+/scalar
diff --git a/contrib/scalar/Makefile b/contrib/scalar/Makefile
new file mode 100644
index 0000000000..231b1ee179
--- /dev/null
+++ b/contrib/scalar/Makefile
@@ -0,0 +1,45 @@
+QUIET_SUBDIR0 = +$(MAKE) -C # space to separate -C and subdir
+QUIET_SUBDIR1 =
+
+ifneq ($(findstring s,$(MAKEFLAGS)),s)
+ifndef V
+ QUIET_GEN = @echo ' ' GEN $@;
+ QUIET_SUBDIR0 = +@subdir=
+ QUIET_SUBDIR1 = ;$(NO_SUBDIR) echo ' ' SUBDIR $$subdir; \
+ $(MAKE) $(PRINT_DIR) -C $$subdir
+else
+ export V
+endif
+endif
+
+all:
+
+include ../../config.mak.uname
+-include ../../config.mak.autogen
+-include ../../config.mak
+
+TARGETS = scalar$(X) scalar.o
+GITLIBS = ../../common-main.o ../../libgit.a ../../xdiff/lib.a
+
+all: scalar$(X) ../../bin-wrappers/scalar
+
+$(GITLIBS):
+ $(QUIET_SUBDIR0)../.. $(QUIET_SUBDIR1) $(subst ../../,,$@)
+
+$(TARGETS): $(GITLIBS) scalar.c
+ $(QUIET_SUBDIR0)../.. $(QUIET_SUBDIR1) $(patsubst %,contrib/scalar/%,$@)
+
+clean:
+ $(RM) $(TARGETS) ../../bin-wrappers/scalar
+
+../../bin-wrappers/scalar: ../../wrap-for-bin.sh Makefile
+ @mkdir -p ../../bin-wrappers
+ $(QUIET_GEN)sed -e '1s|#!.*/sh|#!$(SHELL_PATH_SQ)|' \
+ -e 's|@@BUILD_DIR@@|$(shell cd ../.. && pwd)|' \
+ -e 's|@@PROG@@|contrib/scalar/scalar$(X)|' < $< > $@ && \
+ chmod +x $@
+
+test: all
+ $(MAKE) -C t
+
+.PHONY: $(GITLIBS) all clean test FORCE
diff --git a/contrib/scalar/README.md b/contrib/scalar/README.md
new file mode 100644
index 0000000000..634b5771ed
--- /dev/null
+++ b/contrib/scalar/README.md
@@ -0,0 +1,82 @@
+# Scalar - an opinionated repository management tool
+
+Scalar is an add-on to Git that helps users take advantage of advanced
+performance features in Git. Originally implemented in C# using .NET Core,
+based on the learnings from the VFS for Git project, most of the techniques
+developed by the Scalar project have been integrated into core Git already:
+
+* partial clone,
+* commit graphs,
+* multi-pack index,
+* sparse checkout (cone mode),
+* scheduled background maintenance,
+* etc
+
+This directory contains the remaining parts of Scalar that are not (yet) in
+core Git.
+
+## Roadmap
+
+The idea is to populate this directory via incremental patch series and
+eventually move to a top-level directory next to `gitk-git/` and to `git-gui/`. The
+current plan involves the following patch series:
+
+- `scalar-the-beginning`: The initial patch series which sets up
+ `contrib/scalar/` and populates it with a minimal `scalar` command that
+ demonstrates the fundamental ideas.
+
+- `scalar-c-and-C`: The `scalar` command learns about two options that can be
+ specified before the command, `-c <key>=<value>` and `-C <directory>`.
+
+- `scalar-diagnose`: The `scalar` command is taught the `diagnose` subcommand.
+
+- `scalar-and-builtin-fsmonitor`: The built-in FSMonitor is enabled in `scalar
+ register` and in `scalar clone`, for an enormous performance boost when
+ working in large worktrees. This patch series necessarily depends on Jeff
+ Hostetler's FSMonitor patch series to be integrated into Git.
+
+- `scalar-gentler-config-locking`: Scalar enlistments are registered in the
+ user's Git config. This usually does not represent any problem because it is
+ rare for a user to register an enlistment. However, in Scalar's functional
+ tests, Scalar enlistments are created galore, and in parallel, which can lead
+ to lock contention. This patch series works around that problem by re-trying
+ to lock the config file in a gentle fashion.
+
+- `scalar-extra-docs`: Add some extensive documentation that has been written
+ in the original Scalar project (all subject to discussion, of course).
+
+- `optionally-install-scalar`: Now that Scalar is feature (and documentation)
+ complete and is verified in CI builds, let's offer to install it.
+
+- `move-scalar-to-toplevel`: Now that Scalar is complete, let's move it next to
+ `gitk-git/` and to `git-gui/`, making it a top-level command.
+
+The following two patch series exist in Microsoft's fork of Git and are
+publicly available. There is no current plan to upstream them, not because I
+want to withhold these patches, but because I don't think the Git community is
+interested in these patches.
+
+There are some interesting ideas there, but the implementation is too specific
+to Azure Repos and/or VFS for Git to be of much help in general (and also: my
+colleagues tried to upstream some patches already and the enthusiasm for
+integrating things related to Azure Repos and VFS for Git can be summarized in
+very, very few words).
+
+These still exist mainly because the GVFS protocol is what Azure Repos has
+instead of partial clone, while Git is focused on improving partial clone:
+
+- `scalar-with-gvfs`: The primary purpose of this patch series is to support
+ existing Scalar users whose repositories are hosted in Azure Repos (which
+ does not support Git's partial clones, but supports its predecessor, the GVFS
+ protocol, which is used by Scalar to emulate the partial clone).
+
+ Since the GVFS protocol will never be supported by core Git, this patch
+ series will remain in Microsoft's fork of Git.
+
+- `run-scalar-functional-tests`: The Scalar project developed a quite
+ comprehensive set of integration tests (or, "Functional Tests"). They are the
+ sole remaining part of the original C#-based Scalar project, and this patch
+ adds a GitHub workflow that runs them all.
+
+ Since the tests partially depend on features that are only provided in the
+ `scalar-with-gvfs` patch series, this patch cannot be upstreamed.
diff --git a/contrib/scalar/scalar.c b/contrib/scalar/scalar.c
new file mode 100644
index 0000000000..1ce9c2b00e
--- /dev/null
+++ b/contrib/scalar/scalar.c
@@ -0,0 +1,826 @@
+/*
+ * The Scalar command-line interface.
+ */
+
+#include "cache.h"
+#include "gettext.h"
+#include "parse-options.h"
+#include "config.h"
+#include "run-command.h"
+#include "refs.h"
+#include "dir.h"
+#include "packfile.h"
+#include "help.h"
+
+/*
+ * Remove the deepest subdirectory in the provided path string. Path must not
+ * include a trailing path separator. Returns 1 if parent directory found,
+ * otherwise 0.
+ */
+static int strbuf_parent_directory(struct strbuf *buf)
+{
+ size_t len = buf->len;
+ size_t offset = offset_1st_component(buf->buf);
+ char *path_sep = find_last_dir_sep(buf->buf + offset);
+ strbuf_setlen(buf, path_sep ? path_sep - buf->buf : offset);
+
+ return buf->len < len;
+}
+
+static void setup_enlistment_directory(int argc, const char **argv,
+ const char * const *usagestr,
+ const struct option *options,
+ struct strbuf *enlistment_root)
+{
+ struct strbuf path = STRBUF_INIT;
+ char *root;
+ int enlistment_found = 0;
+
+ if (startup_info->have_repository)
+ BUG("gitdir already set up?!?");
+
+ if (argc > 1)
+ usage_with_options(usagestr, options);
+
+ /* find the worktree, determine its corresponding root */
+ if (argc == 1)
+ strbuf_add_absolute_path(&path, argv[0]);
+ else if (strbuf_getcwd(&path) < 0)
+ die(_("need a working directory"));
+
+ strbuf_trim_trailing_dir_sep(&path);
+ do {
+ const size_t len = path.len;
+
+ /* check if currently in enlistment root with src/ workdir */
+ strbuf_addstr(&path, "/src");
+ if (is_nonbare_repository_dir(&path)) {
+ if (enlistment_root)
+ strbuf_add(enlistment_root, path.buf, len);
+
+ enlistment_found = 1;
+ break;
+ }
+
+ /* reset to original path */
+ strbuf_setlen(&path, len);
+
+ /* check if currently in workdir */
+ if (is_nonbare_repository_dir(&path)) {
+ if (enlistment_root) {
+ /*
+ * If the worktree's directory's name is `src`, the enlistment is the
+ * parent directory, otherwise it is identical to the worktree.
+ */
+ root = strip_path_suffix(path.buf, "src");
+ strbuf_addstr(enlistment_root, root ? root : path.buf);
+ free(root);
+ }
+
+ enlistment_found = 1;
+ break;
+ }
+ } while (strbuf_parent_directory(&path));
+
+ if (!enlistment_found)
+ die(_("could not find enlistment root"));
+
+ if (chdir(path.buf) < 0)
+ die_errno(_("could not switch to '%s'"), path.buf);
+
+ strbuf_release(&path);
+ setup_git_directory();
+}
+
+static int run_git(const char *arg, ...)
+{
+ struct strvec argv = STRVEC_INIT;
+ va_list args;
+ const char *p;
+ int res;
+
+ va_start(args, arg);
+ strvec_push(&argv, arg);
+ while ((p = va_arg(args, const char *)))
+ strvec_push(&argv, p);
+ va_end(args);
+
+ res = run_command_v_opt(argv.v, RUN_GIT_CMD);
+
+ strvec_clear(&argv);
+ return res;
+}
+
+static int set_recommended_config(int reconfigure)
+{
+ struct {
+ const char *key;
+ const char *value;
+ int overwrite_on_reconfigure;
+ } config[] = {
+ /* Required */
+ { "am.keepCR", "true", 1 },
+ { "core.FSCache", "true", 1 },
+ { "core.multiPackIndex", "true", 1 },
+ { "core.preloadIndex", "true", 1 },
+#ifndef WIN32
+ { "core.untrackedCache", "true", 1 },
+#else
+ /*
+ * Unfortunately, Scalar's Functional Tests demonstrated
+ * that the untracked cache feature is unreliable on Windows
+ * (which is a bummer because that platform would benefit the
+ * most from it). For some reason, freshly created files seem
+ * not to update the directory's `lastModified` time
+ * immediately, but the untracked cache would need to rely on
+ * that.
+ *
+ * Therefore, with a sad heart, we disable this very useful
+ * feature on Windows.
+ */
+ { "core.untrackedCache", "false", 1 },
+#endif
+ { "core.logAllRefUpdates", "true", 1 },
+ { "credential.https://dev.azure.com.useHttpPath", "true", 1 },
+ { "credential.validate", "false", 1 }, /* GCM4W-only */
+ { "gc.auto", "0", 1 },
+ { "gui.GCWarning", "false", 1 },
+ { "index.threads", "true", 1 },
+ { "index.version", "4", 1 },
+ { "merge.stat", "false", 1 },
+ { "merge.renames", "true", 1 },
+ { "pack.useBitmaps", "false", 1 },
+ { "pack.useSparse", "true", 1 },
+ { "receive.autoGC", "false", 1 },
+ { "reset.quiet", "true", 1 },
+ { "feature.manyFiles", "false", 1 },
+ { "feature.experimental", "false", 1 },
+ { "fetch.unpackLimit", "1", 1 },
+ { "fetch.writeCommitGraph", "false", 1 },
+#ifdef WIN32
+ { "http.sslBackend", "schannel", 1 },
+#endif
+ /* Optional */
+ { "status.aheadBehind", "false" },
+ { "commitGraph.generationVersion", "1" },
+ { "core.autoCRLF", "false" },
+ { "core.safeCRLF", "false" },
+ { "fetch.showForcedUpdates", "false" },
+ { NULL, NULL },
+ };
+ int i;
+ char *value;
+
+ for (i = 0; config[i].key; i++) {
+ if ((reconfigure && config[i].overwrite_on_reconfigure) ||
+ git_config_get_string(config[i].key, &value)) {
+ trace2_data_string("scalar", the_repository, config[i].key, "created");
+ if (git_config_set_gently(config[i].key,
+ config[i].value) < 0)
+ return error(_("could not configure %s=%s"),
+ config[i].key, config[i].value);
+ } else {
+ trace2_data_string("scalar", the_repository, config[i].key, "exists");
+ free(value);
+ }
+ }
+
+ /*
+ * The `log.excludeDecoration` setting is special because it allows
+ * for multiple values.
+ */
+ if (git_config_get_string("log.excludeDecoration", &value)) {
+ trace2_data_string("scalar", the_repository,
+ "log.excludeDecoration", "created");
+ if (git_config_set_multivar_gently("log.excludeDecoration",
+ "refs/prefetch/*",
+ CONFIG_REGEX_NONE, 0))
+ return error(_("could not configure "
+ "log.excludeDecoration"));
+ } else {
+ trace2_data_string("scalar", the_repository,
+ "log.excludeDecoration", "exists");
+ free(value);
+ }
+
+ return 0;
+}
+
+static int toggle_maintenance(int enable)
+{
+ return run_git("maintenance", enable ? "start" : "unregister", NULL);
+}
+
+static int add_or_remove_enlistment(int add)
+{
+ int res;
+
+ if (!the_repository->worktree)
+ die(_("Scalar enlistments require a worktree"));
+
+ res = run_git("config", "--global", "--get", "--fixed-value",
+ "scalar.repo", the_repository->worktree, NULL);
+
+ /*
+ * If we want to add and the setting is already there, then do nothing.
+ * If we want to remove and the setting is not there, then do nothing.
+ */
+ if ((add && !res) || (!add && res))
+ return 0;
+
+ return run_git("config", "--global", add ? "--add" : "--unset",
+ add ? "--no-fixed-value" : "--fixed-value",
+ "scalar.repo", the_repository->worktree, NULL);
+}
+
+static int register_dir(void)
+{
+ int res = add_or_remove_enlistment(1);
+
+ if (!res)
+ res = set_recommended_config(0);
+
+ if (!res)
+ res = toggle_maintenance(1);
+
+ return res;
+}
+
+static int unregister_dir(void)
+{
+ int res = 0;
+
+ if (toggle_maintenance(0) < 0)
+ res = -1;
+
+ if (add_or_remove_enlistment(0) < 0)
+ res = -1;
+
+ return res;
+}
+
+/* printf-style interface, expects `<key>=<value>` argument */
+static int set_config(const char *fmt, ...)
+{
+ struct strbuf buf = STRBUF_INIT;
+ char *value;
+ int res;
+ va_list args;
+
+ va_start(args, fmt);
+ strbuf_vaddf(&buf, fmt, args);
+ va_end(args);
+
+ value = strchr(buf.buf, '=');
+ if (value)
+ *(value++) = '\0';
+ res = git_config_set_gently(buf.buf, value);
+ strbuf_release(&buf);
+
+ return res;
+}
+
+static char *remote_default_branch(const char *url)
+{
+ struct child_process cp = CHILD_PROCESS_INIT;
+ struct strbuf out = STRBUF_INIT;
+
+ cp.git_cmd = 1;
+ strvec_pushl(&cp.args, "ls-remote", "--symref", url, "HEAD", NULL);
+ if (!pipe_command(&cp, NULL, 0, &out, 0, NULL, 0)) {
+ const char *line = out.buf;
+
+ while (*line) {
+ const char *eol = strchrnul(line, '\n'), *p;
+ size_t len = eol - line;
+ char *branch;
+
+ if (!skip_prefix(line, "ref: ", &p) ||
+ !strip_suffix_mem(line, &len, "\tHEAD")) {
+ line = eol + (*eol == '\n');
+ continue;
+ }
+
+ eol = line + len;
+ if (skip_prefix(p, "refs/heads/", &p)) {
+ branch = xstrndup(p, eol - p);
+ strbuf_release(&out);
+ return branch;
+ }
+
+ error(_("remote HEAD is not a branch: '%.*s'"),
+ (int)(eol - p), p);
+ strbuf_release(&out);
+ return NULL;
+ }
+ }
+ warning(_("failed to get default branch name from remote; "
+ "using local default"));
+ strbuf_reset(&out);
+
+ child_process_init(&cp);
+ cp.git_cmd = 1;
+ strvec_pushl(&cp.args, "symbolic-ref", "--short", "HEAD", NULL);
+ if (!pipe_command(&cp, NULL, 0, &out, 0, NULL, 0)) {
+ strbuf_trim(&out);
+ return strbuf_detach(&out, NULL);
+ }
+
+ strbuf_release(&out);
+ error(_("failed to get default branch name"));
+ return NULL;
+}
+
+static int delete_enlistment(struct strbuf *enlistment)
+{
+#ifdef WIN32
+ struct strbuf parent = STRBUF_INIT;
+#endif
+
+ if (unregister_dir())
+ die(_("failed to unregister repository"));
+
+#ifdef WIN32
+ /*
+ * Change the current directory to one outside of the enlistment so
+ * that we may delete everything underneath it.
+ */
+ strbuf_addbuf(&parent, enlistment);
+ strbuf_parent_directory(&parent);
+ if (chdir(parent.buf) < 0)
+ die_errno(_("could not switch to '%s'"), parent.buf);
+ strbuf_release(&parent);
+#endif
+
+ if (remove_dir_recursively(enlistment, 0))
+ die(_("failed to delete enlistment directory"));
+
+ return 0;
+}
+
+/*
+ * Dummy implementation; Using `get_version_info()` would cause a link error
+ * without this.
+ */
+void load_builtin_commands(const char *prefix, struct cmdnames *cmds)
+{
+ die("not implemented");
+}
+
+static int cmd_clone(int argc, const char **argv)
+{
+ const char *branch = NULL;
+ int full_clone = 0, single_branch = 0;
+ struct option clone_options[] = {
+ OPT_STRING('b', "branch", &branch, N_("<branch>"),
+ N_("branch to checkout after clone")),
+ OPT_BOOL(0, "full-clone", &full_clone,
+ N_("when cloning, create full working directory")),
+ OPT_BOOL(0, "single-branch", &single_branch,
+ N_("only download metadata for the branch that will "
+ "be checked out")),
+ OPT_END(),
+ };
+ const char * const clone_usage[] = {
+ N_("scalar clone [<options>] [--] <repo> [<dir>]"),
+ NULL
+ };
+ const char *url;
+ char *enlistment = NULL, *dir = NULL;
+ struct strbuf buf = STRBUF_INIT;
+ int res;
+
+ argc = parse_options(argc, argv, NULL, clone_options, clone_usage, 0);
+
+ if (argc == 2) {
+ url = argv[0];
+ enlistment = xstrdup(argv[1]);
+ } else if (argc == 1) {
+ url = argv[0];
+
+ strbuf_addstr(&buf, url);
+ /* Strip trailing slashes, if any */
+ while (buf.len > 0 && is_dir_sep(buf.buf[buf.len - 1]))
+ strbuf_setlen(&buf, buf.len - 1);
+ /* Strip suffix `.git`, if any */
+ strbuf_strip_suffix(&buf, ".git");
+
+ enlistment = find_last_dir_sep(buf.buf);
+ if (!enlistment) {
+ die(_("cannot deduce worktree name from '%s'"), url);
+ }
+ enlistment = xstrdup(enlistment + 1);
+ } else {
+ usage_msg_opt(_("You must specify a repository to clone."),
+ clone_usage, clone_options);
+ }
+
+ if (is_directory(enlistment))
+ die(_("directory '%s' exists already"), enlistment);
+
+ dir = xstrfmt("%s/src", enlistment);
+
+ strbuf_reset(&buf);
+ if (branch)
+ strbuf_addf(&buf, "init.defaultBranch=%s", branch);
+ else {
+ char *b = repo_default_branch_name(the_repository, 1);
+ strbuf_addf(&buf, "init.defaultBranch=%s", b);
+ free(b);
+ }
+
+ if ((res = run_git("-c", buf.buf, "init", "--", dir, NULL)))
+ goto cleanup;
+
+ if (chdir(dir) < 0) {
+ res = error_errno(_("could not switch to '%s'"), dir);
+ goto cleanup;
+ }
+
+ setup_git_directory();
+
+ /* common-main already logs `argv` */
+ trace2_def_repo(the_repository);
+
+ if (!branch && !(branch = remote_default_branch(url))) {
+ res = error(_("failed to get default branch for '%s'"), url);
+ goto cleanup;
+ }
+
+ if (set_config("remote.origin.url=%s", url) ||
+ set_config("remote.origin.fetch="
+ "+refs/heads/%s:refs/remotes/origin/%s",
+ single_branch ? branch : "*",
+ single_branch ? branch : "*") ||
+ set_config("remote.origin.promisor=true") ||
+ set_config("remote.origin.partialCloneFilter=blob:none")) {
+ res = error(_("could not configure remote in '%s'"), dir);
+ goto cleanup;
+ }
+
+ if (!full_clone &&
+ (res = run_git("sparse-checkout", "init", "--cone", NULL)))
+ goto cleanup;
+
+ if (set_recommended_config(0))
+ return error(_("could not configure '%s'"), dir);
+
+ if ((res = run_git("fetch", "--quiet", "origin", NULL))) {
+ warning(_("partial clone failed; attempting full clone"));
+
+ if (set_config("remote.origin.promisor") ||
+ set_config("remote.origin.partialCloneFilter")) {
+ res = error(_("could not configure for full clone"));
+ goto cleanup;
+ }
+
+ if ((res = run_git("fetch", "--quiet", "origin", NULL)))
+ goto cleanup;
+ }
+
+ if ((res = set_config("branch.%s.remote=origin", branch)))
+ goto cleanup;
+ if ((res = set_config("branch.%s.merge=refs/heads/%s",
+ branch, branch)))
+ goto cleanup;
+
+ strbuf_reset(&buf);
+ strbuf_addf(&buf, "origin/%s", branch);
+ res = run_git("checkout", "-f", "-t", buf.buf, NULL);
+ if (res)
+ goto cleanup;
+
+ res = register_dir();
+
+cleanup:
+ free(enlistment);
+ free(dir);
+ strbuf_release(&buf);
+ return res;
+}
+
+static int cmd_list(int argc, const char **argv)
+{
+ if (argc != 1)
+ die(_("`scalar list` does not take arguments"));
+
+ if (run_git("config", "--global", "--get-all", "scalar.repo", NULL) < 0)
+ return -1;
+ return 0;
+}
+
+static int cmd_register(int argc, const char **argv)
+{
+ struct option options[] = {
+ OPT_END(),
+ };
+ const char * const usage[] = {
+ N_("scalar register [<enlistment>]"),
+ NULL
+ };
+
+ argc = parse_options(argc, argv, NULL, options,
+ usage, 0);
+
+ setup_enlistment_directory(argc, argv, usage, options, NULL);
+
+ return register_dir();
+}
+
+static int get_scalar_repos(const char *key, const char *value, void *data)
+{
+ struct string_list *list = data;
+
+ if (!strcmp(key, "scalar.repo"))
+ string_list_append(list, value);
+
+ return 0;
+}
+
+static int cmd_reconfigure(int argc, const char **argv)
+{
+ int all = 0;
+ struct option options[] = {
+ OPT_BOOL('a', "all", &all,
+ N_("reconfigure all registered enlistments")),
+ OPT_END(),
+ };
+ const char * const usage[] = {
+ N_("scalar reconfigure [--all | <enlistment>]"),
+ NULL
+ };
+ struct string_list scalar_repos = STRING_LIST_INIT_DUP;
+ int i, res = 0;
+ struct repository r = { NULL };
+ struct strbuf commondir = STRBUF_INIT, gitdir = STRBUF_INIT;
+
+ argc = parse_options(argc, argv, NULL, options,
+ usage, 0);
+
+ if (!all) {
+ setup_enlistment_directory(argc, argv, usage, options, NULL);
+
+ return set_recommended_config(1);
+ }
+
+ if (argc > 0)
+ usage_msg_opt(_("--all or <enlistment>, but not both"),
+ usage, options);
+
+ git_config(get_scalar_repos, &scalar_repos);
+
+ for (i = 0; i < scalar_repos.nr; i++) {
+ const char *dir = scalar_repos.items[i].string;
+
+ strbuf_reset(&commondir);
+ strbuf_reset(&gitdir);
+
+ if (chdir(dir) < 0) {
+ warning_errno(_("could not switch to '%s'"), dir);
+ res = -1;
+ } else if (discover_git_directory(&commondir, &gitdir) < 0) {
+ warning_errno(_("git repository gone in '%s'"), dir);
+ res = -1;
+ } else {
+ git_config_clear();
+
+ the_repository = &r;
+ r.commondir = commondir.buf;
+ r.gitdir = gitdir.buf;
+
+ if (set_recommended_config(1) < 0)
+ res = -1;
+ }
+ }
+
+ string_list_clear(&scalar_repos, 1);
+ strbuf_release(&commondir);
+ strbuf_release(&gitdir);
+
+ return res;
+}
+
+static int cmd_run(int argc, const char **argv)
+{
+ struct option options[] = {
+ OPT_END(),
+ };
+ struct {
+ const char *arg, *task;
+ } tasks[] = {
+ { "config", NULL },
+ { "commit-graph", "commit-graph" },
+ { "fetch", "prefetch" },
+ { "loose-objects", "loose-objects" },
+ { "pack-files", "incremental-repack" },
+ { NULL, NULL }
+ };
+ struct strbuf buf = STRBUF_INIT;
+ const char *usagestr[] = { NULL, NULL };
+ int i;
+
+ strbuf_addstr(&buf, N_("scalar run <task> [<enlistment>]\nTasks:\n"));
+ for (i = 0; tasks[i].arg; i++)
+ strbuf_addf(&buf, "\t%s\n", tasks[i].arg);
+ usagestr[0] = buf.buf;
+
+ argc = parse_options(argc, argv, NULL, options,
+ usagestr, 0);
+
+ if (!argc)
+ usage_with_options(usagestr, options);
+
+ if (!strcmp("all", argv[0])) {
+ i = -1;
+ } else {
+ for (i = 0; tasks[i].arg && strcmp(tasks[i].arg, argv[0]); i++)
+ ; /* keep looking for the task */
+
+ if (i > 0 && !tasks[i].arg) {
+ error(_("no such task: '%s'"), argv[0]);
+ usage_with_options(usagestr, options);
+ }
+ }
+
+ argc--;
+ argv++;
+ setup_enlistment_directory(argc, argv, usagestr, options, NULL);
+ strbuf_release(&buf);
+
+ if (i == 0)
+ return register_dir();
+
+ if (i > 0)
+ return run_git("maintenance", "run",
+ "--task", tasks[i].task, NULL);
+
+ if (register_dir())
+ return -1;
+ for (i = 1; tasks[i].arg; i++)
+ if (run_git("maintenance", "run",
+ "--task", tasks[i].task, NULL))
+ return -1;
+ return 0;
+}
+
+static int remove_deleted_enlistment(struct strbuf *path)
+{
+ int res = 0;
+ strbuf_realpath_forgiving(path, path->buf, 1);
+
+ if (run_git("config", "--global",
+ "--unset", "--fixed-value",
+ "scalar.repo", path->buf, NULL) < 0)
+ res = -1;
+
+ if (run_git("config", "--global",
+ "--unset", "--fixed-value",
+ "maintenance.repo", path->buf, NULL) < 0)
+ res = -1;
+
+ return res;
+}
+
+static int cmd_unregister(int argc, const char **argv)
+{
+ struct option options[] = {
+ OPT_END(),
+ };
+ const char * const usage[] = {
+ N_("scalar unregister [<enlistment>]"),
+ NULL
+ };
+
+ argc = parse_options(argc, argv, NULL, options,
+ usage, 0);
+
+ /*
+ * Be forgiving when the enlistment or worktree does not even exist any
+ * longer; This can be the case if a user deleted the worktree by
+ * mistake and _still_ wants to unregister the thing.
+ */
+ if (argc == 1) {
+ struct strbuf src_path = STRBUF_INIT, workdir_path = STRBUF_INIT;
+
+ strbuf_addf(&src_path, "%s/src/.git", argv[0]);
+ strbuf_addf(&workdir_path, "%s/.git", argv[0]);
+ if (!is_directory(src_path.buf) && !is_directory(workdir_path.buf)) {
+ /* remove possible matching registrations */
+ int res = -1;
+
+ strbuf_strip_suffix(&src_path, "/.git");
+ res = remove_deleted_enlistment(&src_path) && res;
+
+ strbuf_strip_suffix(&workdir_path, "/.git");
+ res = remove_deleted_enlistment(&workdir_path) && res;
+
+ strbuf_release(&src_path);
+ strbuf_release(&workdir_path);
+ return res;
+ }
+ strbuf_release(&src_path);
+ strbuf_release(&workdir_path);
+ }
+
+ setup_enlistment_directory(argc, argv, usage, options, NULL);
+
+ return unregister_dir();
+}
+
+static int cmd_delete(int argc, const char **argv)
+{
+ char *cwd = xgetcwd();
+ struct option options[] = {
+ OPT_END(),
+ };
+ const char * const usage[] = {
+ N_("scalar delete <enlistment>"),
+ NULL
+ };
+ struct strbuf enlistment = STRBUF_INIT;
+ int res = 0;
+
+ argc = parse_options(argc, argv, NULL, options,
+ usage, 0);
+
+ if (argc != 1)
+ usage_with_options(usage, options);
+
+ setup_enlistment_directory(argc, argv, usage, options, &enlistment);
+
+ if (dir_inside_of(cwd, enlistment.buf) >= 0)
+ res = error(_("refusing to delete current working directory"));
+ else {
+ close_object_store(the_repository->objects);
+ res = delete_enlistment(&enlistment);
+ }
+ strbuf_release(&enlistment);
+ free(cwd);
+
+ return res;
+}
+
+static int cmd_version(int argc, const char **argv)
+{
+ int verbose = 0, build_options = 0;
+ struct option options[] = {
+ OPT__VERBOSE(&verbose, N_("include Git version")),
+ OPT_BOOL(0, "build-options", &build_options,
+ N_("include Git's build options")),
+ OPT_END(),
+ };
+ const char * const usage[] = {
+ N_("scalar verbose [-v | --verbose] [--build-options]"),
+ NULL
+ };
+ struct strbuf buf = STRBUF_INIT;
+
+ argc = parse_options(argc, argv, NULL, options,
+ usage, 0);
+
+ if (argc != 0)
+ usage_with_options(usage, options);
+
+ get_version_info(&buf, build_options);
+ fprintf(stderr, "%s\n", buf.buf);
+ strbuf_release(&buf);
+
+ return 0;
+}
+
+static struct {
+ const char *name;
+ int (*fn)(int, const char **);
+} builtins[] = {
+ { "clone", cmd_clone },
+ { "list", cmd_list },
+ { "register", cmd_register },
+ { "unregister", cmd_unregister },
+ { "run", cmd_run },
+ { "reconfigure", cmd_reconfigure },
+ { "delete", cmd_delete },
+ { "version", cmd_version },
+ { NULL, NULL},
+};
+
+int cmd_main(int argc, const char **argv)
+{
+ struct strbuf scalar_usage = STRBUF_INIT;
+ int i;
+
+ if (argc > 1) {
+ argv++;
+ argc--;
+
+ for (i = 0; builtins[i].name; i++)
+ if (!strcmp(builtins[i].name, argv[0]))
+ return !!builtins[i].fn(argc, argv);
+ }
+
+ strbuf_addstr(&scalar_usage,
+ N_("scalar <command> [<options>]\n\nCommands:\n"));
+ for (i = 0; builtins[i].name; i++)
+ strbuf_addf(&scalar_usage, "\t%s\n", builtins[i].name);
+
+ usage(scalar_usage.buf);
+}
diff --git a/contrib/scalar/scalar.txt b/contrib/scalar/scalar.txt
new file mode 100644
index 0000000000..f416d63728
--- /dev/null
+++ b/contrib/scalar/scalar.txt
@@ -0,0 +1,145 @@
+scalar(1)
+=========
+
+NAME
+----
+scalar - an opinionated repository management tool
+
+SYNOPSIS
+--------
+[verse]
+scalar clone [--single-branch] [--branch <main-branch>] [--full-clone] <url> [<enlistment>]
+scalar list
+scalar register [<enlistment>]
+scalar unregister [<enlistment>]
+scalar run ( all | config | commit-graph | fetch | loose-objects | pack-files ) [<enlistment>]
+scalar reconfigure [ --all | <enlistment> ]
+scalar delete <enlistment>
+
+DESCRIPTION
+-----------
+
+Scalar is an opinionated repository management tool. By creating new
+repositories or registering existing repositories with Scalar, your Git
+experience will speed up. Scalar sets advanced Git config settings,
+maintains your repositories in the background, and helps reduce data sent
+across the network.
+
+An important Scalar concept is the enlistment: this is the top-level directory
+of the project. It usually contains the subdirectory `src/` which is a Git
+worktree. This encourages the separation between tracked files (inside `src/`)
+and untracked files, such as build artifacts (outside `src/`). When registering
+an existing Git worktree with Scalar whose name is not `src`, the enlistment
+will be identical to the worktree.
+
+The `scalar` command implements various subcommands, and different options
+depending on the subcommand. With the exception of `clone`, `list` and
+`reconfigure --all`, all subcommands expect to be run in an enlistment.
+
+COMMANDS
+--------
+
+Clone
+~~~~~
+
+clone [<options>] <url> [<enlistment>]::
+ Clones the specified repository, similar to linkgit:git-clone[1]. By
+ default, only commit and tree objects are cloned. Once finished, the
+ worktree is located at `<enlistment>/src`.
++
+The sparse-checkout feature is enabled (except when run with `--full-clone`)
+and the only files present are those in the top-level directory. Use
+`git sparse-checkout set` to expand the set of directories you want to see,
+or `git sparse-checkout disable` to expand to all files (see
+linkgit:git-sparse-checkout[1] for more details). You can explore the
+subdirectories outside your sparse-checkout by using `git ls-tree
+HEAD[:<directory>]`.
+
+-b <name>::
+--branch <name>::
+ Instead of checking out the branch pointed to by the cloned
+ repository's HEAD, check out the `<name>` branch instead.
+
+--[no-]single-branch::
+ Clone only the history leading to the tip of a single branch, either
+ specified by the `--branch` option or the primary branch remote's
+ `HEAD` points at.
++
+Further fetches into the resulting repository will only update the
+remote-tracking branch for the branch this option was used for the initial
+cloning. If the HEAD at the remote did not point at any branch when
+`--single-branch` clone was made, no remote-tracking branch is created.
+
+--[no-]full-clone::
+ A sparse-checkout is initialized by default. This behavior can be
+ turned off via `--full-clone`.
+
+List
+~~~~
+
+list::
+ List enlistments that are currently registered by Scalar. This
+ subcommand does not need to be run inside an enlistment.
+
+Register
+~~~~~~~~
+
+register [<enlistment>]::
+ Adds the enlistment's repository to the list of registered repositories
+ and starts background maintenance. If `<enlistment>` is not provided,
+ then the enlistment associated with the current working directory is
+ registered.
++
+Note: when this subcommand is called in a worktree that is called `src/`, its
+parent directory is considered to be the Scalar enlistment. If the worktree is
+_not_ called `src/`, it itself will be considered to be the Scalar enlistment.
+
+Unregister
+~~~~~~~~~~
+
+unregister [<enlistment>]::
+ Remove the specified repository from the list of repositories
+ registered with Scalar and stop the scheduled background maintenance.
+
+Run
+~~~
+
+scalar run ( all | config | commit-graph | fetch | loose-objects | pack-files ) [<enlistment>]::
+ Run the given maintenance task (or all tasks, if `all` was specified).
+ Except for `all` and `config`, this subcommand simply hands off to
+ linkgit:git-maintenance[1] (mapping `fetch` to `prefetch` and
+ `pack-files` to `incremental-repack`).
++
+These tasks are run automatically as part of the scheduled maintenance,
+as soon as the repository is registered with Scalar. It should therefore
+not be necessary to run this subcommand manually.
++
+The `config` task is specific to Scalar and configures all those
+opinionated default settings that make Git work more efficiently with
+large repositories. As this task is run as part of `scalar clone`
+automatically, explicit invocations of this task are rarely needed.
+
+Reconfigure
+~~~~~~~~~~~
+
+After a Scalar upgrade, or when the configuration of a Scalar enlistment
+was somehow corrupted or changed by mistake, this subcommand allows to
+reconfigure the enlistment.
+
+With the `--all` option, all enlistments currently registered with Scalar
+will be reconfigured. Use this option after each Scalar upgrade.
+
+Delete
+~~~~~~
+
+delete <enlistment>::
+ This subcommand lets you delete an existing Scalar enlistment from your
+ local file system, unregistering the repository.
+
+SEE ALSO
+--------
+linkgit:git-clone[1], linkgit:git-maintenance[1].
+
+Scalar
+---
+Associated with the linkgit:git[1] suite
diff --git a/contrib/scalar/t/Makefile b/contrib/scalar/t/Makefile
new file mode 100644
index 0000000000..6170672bb3
--- /dev/null
+++ b/contrib/scalar/t/Makefile
@@ -0,0 +1,78 @@
+# Run scalar tests
+#
+# Copyright (c) 2005,2021 Junio C Hamano, Johannes Schindelin
+#
+
+-include ../../../config.mak.autogen
+-include ../../../config.mak
+
+SHELL_PATH ?= $(SHELL)
+PERL_PATH ?= /usr/bin/perl
+RM ?= rm -f
+PROVE ?= prove
+DEFAULT_TEST_TARGET ?= test
+TEST_LINT ?= test-lint
+
+ifdef TEST_OUTPUT_DIRECTORY
+TEST_RESULTS_DIRECTORY = $(TEST_OUTPUT_DIRECTORY)/test-results
+else
+TEST_RESULTS_DIRECTORY = ../../../t/test-results
+endif
+
+# Shell quote;
+SHELL_PATH_SQ = $(subst ','\'',$(SHELL_PATH))
+PERL_PATH_SQ = $(subst ','\'',$(PERL_PATH))
+TEST_RESULTS_DIRECTORY_SQ = $(subst ','\'',$(TEST_RESULTS_DIRECTORY))
+
+T = $(sort $(wildcard t[0-9][0-9][0-9][0-9]-*.sh))
+
+all: $(DEFAULT_TEST_TARGET)
+
+test: $(TEST_LINT)
+ $(MAKE) aggregate-results-and-cleanup
+
+prove: $(TEST_LINT)
+ @echo "*** prove ***"; GIT_CONFIG=.git/config $(PROVE) --exec '$(SHELL_PATH_SQ)' $(GIT_PROVE_OPTS) $(T) :: $(GIT_TEST_OPTS)
+ $(MAKE) clean-except-prove-cache
+
+$(T):
+ @echo "*** $@ ***"; GIT_CONFIG=.git/config '$(SHELL_PATH_SQ)' $@ $(GIT_TEST_OPTS)
+
+clean-except-prove-cache:
+ $(RM) -r 'trash directory'.* '$(TEST_RESULTS_DIRECTORY_SQ)'
+ $(RM) -r valgrind/bin
+
+clean: clean-except-prove-cache
+ $(RM) .prove
+
+test-lint: test-lint-duplicates test-lint-executable test-lint-shell-syntax
+
+test-lint-duplicates:
+ @dups=`echo $(T) | tr ' ' '\n' | sed 's/-.*//' | sort | uniq -d` && \
+ test -z "$$dups" || { \
+ echo >&2 "duplicate test numbers:" $$dups; exit 1; }
+
+test-lint-executable:
+ @bad=`for i in $(T); do test -x "$$i" || echo $$i; done` && \
+ test -z "$$bad" || { \
+ echo >&2 "non-executable tests:" $$bad; exit 1; }
+
+test-lint-shell-syntax:
+ @'$(PERL_PATH_SQ)' ../../../t/check-non-portable-shell.pl $(T)
+
+aggregate-results-and-cleanup: $(T)
+ $(MAKE) aggregate-results
+ $(MAKE) clean
+
+aggregate-results:
+ for f in '$(TEST_RESULTS_DIRECTORY_SQ)'/t*-*.counts; do \
+ echo "$$f"; \
+ done | '$(SHELL_PATH_SQ)' ../../../t/aggregate-results.sh
+
+valgrind:
+ $(MAKE) GIT_TEST_OPTS="$(GIT_TEST_OPTS) --valgrind"
+
+test-results:
+ mkdir -p test-results
+
+.PHONY: $(T) aggregate-results clean valgrind
diff --git a/contrib/scalar/t/t9099-scalar.sh b/contrib/scalar/t/t9099-scalar.sh
new file mode 100755
index 0000000000..2e1502ad45
--- /dev/null
+++ b/contrib/scalar/t/t9099-scalar.sh
@@ -0,0 +1,88 @@
+#!/bin/sh
+
+test_description='test the `scalar` command'
+
+TEST_DIRECTORY=$PWD/../../../t
+export TEST_DIRECTORY
+
+# Make it work with --no-bin-wrappers
+PATH=$PWD/..:$PATH
+
+. ../../../t/test-lib.sh
+
+GIT_TEST_MAINT_SCHEDULER="crontab:test-tool crontab ../cron.txt,launchctl:true,schtasks:true"
+export GIT_TEST_MAINT_SCHEDULER
+
+test_expect_success 'scalar shows a usage' '
+ test_expect_code 129 scalar -h
+'
+
+test_expect_success 'scalar unregister' '
+ git init vanish/src &&
+ scalar register vanish/src &&
+ git config --get --global --fixed-value \
+ maintenance.repo "$(pwd)/vanish/src" &&
+ scalar list >scalar.repos &&
+ grep -F "$(pwd)/vanish/src" scalar.repos &&
+ rm -rf vanish/src/.git &&
+ scalar unregister vanish &&
+ test_must_fail git config --get --global --fixed-value \
+ maintenance.repo "$(pwd)/vanish/src" &&
+ scalar list >scalar.repos &&
+ ! grep -F "$(pwd)/vanish/src" scalar.repos
+'
+
+test_expect_success 'set up repository to clone' '
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+ git switch -c parallel first &&
+ mkdir -p 1/2 &&
+ test_commit 1/2/3 &&
+ git config uploadPack.allowFilter true &&
+ git config uploadPack.allowAnySHA1InWant true
+'
+
+test_expect_success 'scalar clone' '
+ second=$(git rev-parse --verify second:second.t) &&
+ scalar clone "file://$(pwd)" cloned --single-branch &&
+ (
+ cd cloned/src &&
+
+ git config --get --global --fixed-value maintenance.repo \
+ "$(pwd)" &&
+
+ git for-each-ref --format="%(refname)" refs/remotes/origin/ >actual &&
+ echo "refs/remotes/origin/parallel" >expect &&
+ test_cmp expect actual &&
+
+ test_path_is_missing 1/2 &&
+ test_must_fail git rev-list --missing=print $second &&
+ git rev-list $second &&
+ git cat-file blob $second >actual &&
+ echo "second" >expect &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'scalar reconfigure' '
+ git init one/src &&
+ scalar register one &&
+ git -C one/src config core.preloadIndex false &&
+ scalar reconfigure one &&
+ test true = "$(git -C one/src config core.preloadIndex)" &&
+ git -C one/src config core.preloadIndex false &&
+ scalar reconfigure -a &&
+ test true = "$(git -C one/src config core.preloadIndex)"
+'
+
+test_expect_success 'scalar delete without enlistment shows a usage' '
+ test_expect_code 129 scalar delete
+'
+
+test_expect_success 'scalar delete with enlistment' '
+ scalar delete cloned &&
+ test_path_is_missing cloned
+'
+
+test_done
diff --git a/fmt-merge-msg.c b/fmt-merge-msg.c
index 5216191488..e4f7810be2 100644
--- a/fmt-merge-msg.c
+++ b/fmt-merge-msg.c
@@ -533,8 +533,9 @@ static void fmt_merge_msg_sigs(struct strbuf *out)
else {
buf = payload.buf;
len = payload.len;
- if (check_signature(payload.buf, payload.len, sig.buf,
- sig.len, &sigc) &&
+ sigc.payload_type = SIGNATURE_PAYLOAD_TAG;
+ sigc.payload = strbuf_detach(&payload, &sigc.payload_len);
+ if (check_signature(&sigc, sig.buf, sig.len) &&
!sigc.output)
strbuf_addstr(&sig, "gpg verification failed.\n");
else
diff --git a/git.c b/git.c
index 5ff21be21f..7edafd8ecf 100644
--- a/git.c
+++ b/git.c
@@ -421,27 +421,30 @@ static int run_builtin(struct cmd_struct *p, int argc, const char **argv)
int status, help;
struct stat st;
const char *prefix;
+ int run_setup = (p->option & (RUN_SETUP | RUN_SETUP_GENTLY));
- prefix = NULL;
help = argc == 2 && !strcmp(argv[1], "-h");
- if (!help) {
- if (p->option & RUN_SETUP)
- prefix = setup_git_directory();
- else if (p->option & RUN_SETUP_GENTLY) {
- int nongit_ok;
- prefix = setup_git_directory_gently(&nongit_ok);
- }
- precompose_argv_prefix(argc, argv, NULL);
- if (use_pager == -1 && p->option & (RUN_SETUP | RUN_SETUP_GENTLY) &&
- !(p->option & DELAY_PAGER_CONFIG))
- use_pager = check_pager_config(p->cmd);
- if (use_pager == -1 && p->option & USE_PAGER)
- use_pager = 1;
-
- if ((p->option & (RUN_SETUP | RUN_SETUP_GENTLY)) &&
- startup_info->have_repository) /* get_git_dir() may set up repo, avoid that */
- trace_repo_setup(prefix);
+ if (help && (run_setup & RUN_SETUP))
+ /* demote to GENTLY to allow 'git cmd -h' outside repo */
+ run_setup = RUN_SETUP_GENTLY;
+
+ if (run_setup & RUN_SETUP) {
+ prefix = setup_git_directory();
+ } else if (run_setup & RUN_SETUP_GENTLY) {
+ int nongit_ok;
+ prefix = setup_git_directory_gently(&nongit_ok);
+ } else {
+ prefix = NULL;
}
+ precompose_argv_prefix(argc, argv, NULL);
+ if (use_pager == -1 && run_setup &&
+ !(p->option & DELAY_PAGER_CONFIG))
+ use_pager = check_pager_config(p->cmd);
+ if (use_pager == -1 && p->option & USE_PAGER)
+ use_pager = 1;
+ if (run_setup && startup_info->have_repository)
+ /* get_git_dir() may set up repo, avoid that */
+ trace_repo_setup(prefix);
commit_pager_choice();
if (!help && get_super_prefix()) {
diff --git a/gpg-interface.c b/gpg-interface.c
index 3e7255a2a9..b52eb0e2e0 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -19,8 +19,8 @@ struct gpg_format {
const char **verify_args;
const char **sigs;
int (*verify_signed_buffer)(struct signature_check *sigc,
- struct gpg_format *fmt, const char *payload,
- size_t payload_size, const char *signature,
+ struct gpg_format *fmt,
+ const char *signature,
size_t signature_size);
int (*sign_buffer)(struct strbuf *buffer, struct strbuf *signature,
const char *signing_key);
@@ -53,12 +53,12 @@ static const char *ssh_sigs[] = {
};
static int verify_gpg_signed_buffer(struct signature_check *sigc,
- struct gpg_format *fmt, const char *payload,
- size_t payload_size, const char *signature,
+ struct gpg_format *fmt,
+ const char *signature,
size_t signature_size);
static int verify_ssh_signed_buffer(struct signature_check *sigc,
- struct gpg_format *fmt, const char *payload,
- size_t payload_size, const char *signature,
+ struct gpg_format *fmt,
+ const char *signature,
size_t signature_size);
static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,
const char *signing_key);
@@ -314,8 +314,8 @@ error:
}
static int verify_gpg_signed_buffer(struct signature_check *sigc,
- struct gpg_format *fmt, const char *payload,
- size_t payload_size, const char *signature,
+ struct gpg_format *fmt,
+ const char *signature,
size_t signature_size)
{
struct child_process gpg = CHILD_PROCESS_INIT;
@@ -343,14 +343,13 @@ static int verify_gpg_signed_buffer(struct signature_check *sigc,
NULL);
sigchain_push(SIGPIPE, SIG_IGN);
- ret = pipe_command(&gpg, payload, payload_size, &gpg_stdout, 0,
+ ret = pipe_command(&gpg, sigc->payload, sigc->payload_len, &gpg_stdout, 0,
&gpg_stderr, 0);
sigchain_pop(SIGPIPE);
delete_tempfile(&temp);
ret |= !strstr(gpg_stdout.buf, "\n[GNUPG:] GOODSIG ");
- sigc->payload = xmemdupz(payload, payload_size);
sigc->output = strbuf_detach(&gpg_stderr, NULL);
sigc->gpg_status = strbuf_detach(&gpg_stdout, NULL);
@@ -426,8 +425,8 @@ cleanup:
}
static int verify_ssh_signed_buffer(struct signature_check *sigc,
- struct gpg_format *fmt, const char *payload,
- size_t payload_size, const char *signature,
+ struct gpg_format *fmt,
+ const char *signature,
size_t signature_size)
{
struct child_process ssh_keygen = CHILD_PROCESS_INIT;
@@ -440,6 +439,13 @@ static int verify_ssh_signed_buffer(struct signature_check *sigc,
struct strbuf ssh_principals_err = STRBUF_INIT;
struct strbuf ssh_keygen_out = STRBUF_INIT;
struct strbuf ssh_keygen_err = STRBUF_INIT;
+ struct strbuf verify_time = STRBUF_INIT;
+ const struct date_mode verify_date_mode = {
+ .type = DATE_STRFTIME,
+ .strftime_fmt = "%Y%m%d%H%M%S",
+ /* SSH signing key validity has no timezone information - Use the local timezone */
+ .local = 1,
+ };
if (!ssh_allowed_signers) {
error(_("gpg.ssh.allowedSignersFile needs to be configured and exist for ssh signature verification"));
@@ -457,11 +463,16 @@ static int verify_ssh_signed_buffer(struct signature_check *sigc,
return -1;
}
+ if (sigc->payload_timestamp)
+ strbuf_addf(&verify_time, "-Overify-time=%s",
+ show_date(sigc->payload_timestamp, 0, &verify_date_mode));
+
/* Find the principal from the signers */
strvec_pushl(&ssh_keygen.args, fmt->program,
"-Y", "find-principals",
"-f", ssh_allowed_signers,
"-s", buffer_file->filename.buf,
+ verify_time.buf,
NULL);
ret = pipe_command(&ssh_keygen, NULL, 0, &ssh_principals_out, 0,
&ssh_principals_err, 0);
@@ -479,8 +490,9 @@ static int verify_ssh_signed_buffer(struct signature_check *sigc,
"-Y", "check-novalidate",
"-n", "git",
"-s", buffer_file->filename.buf,
+ verify_time.buf,
NULL);
- pipe_command(&ssh_keygen, payload, payload_size,
+ pipe_command(&ssh_keygen, sigc->payload, sigc->payload_len,
&ssh_keygen_out, 0, &ssh_keygen_err, 0);
/*
@@ -513,6 +525,7 @@ static int verify_ssh_signed_buffer(struct signature_check *sigc,
"-f", ssh_allowed_signers,
"-I", principal,
"-s", buffer_file->filename.buf,
+ verify_time.buf,
NULL);
if (ssh_revocation_file) {
@@ -526,7 +539,7 @@ static int verify_ssh_signed_buffer(struct signature_check *sigc,
}
sigchain_push(SIGPIPE, SIG_IGN);
- ret = pipe_command(&ssh_keygen, payload, payload_size,
+ ret = pipe_command(&ssh_keygen, sigc->payload, sigc->payload_len,
&ssh_keygen_out, 0, &ssh_keygen_err, 0);
sigchain_pop(SIGPIPE);
@@ -540,7 +553,6 @@ static int verify_ssh_signed_buffer(struct signature_check *sigc,
}
}
- sigc->payload = xmemdupz(payload, payload_size);
strbuf_stripspace(&ssh_keygen_out, 0);
strbuf_stripspace(&ssh_keygen_err, 0);
/* Add stderr outputs to show the user actual ssh-keygen errors */
@@ -558,12 +570,48 @@ out:
strbuf_release(&ssh_principals_err);
strbuf_release(&ssh_keygen_out);
strbuf_release(&ssh_keygen_err);
+ strbuf_release(&verify_time);
return ret;
}
-int check_signature(const char *payload, size_t plen, const char *signature,
- size_t slen, struct signature_check *sigc)
+static int parse_payload_metadata(struct signature_check *sigc)
+{
+ const char *ident_line = NULL;
+ size_t ident_len;
+ struct ident_split ident;
+ const char *signer_header;
+
+ switch (sigc->payload_type) {
+ case SIGNATURE_PAYLOAD_COMMIT:
+ signer_header = "committer";
+ break;
+ case SIGNATURE_PAYLOAD_TAG:
+ signer_header = "tagger";
+ break;
+ case SIGNATURE_PAYLOAD_UNDEFINED:
+ case SIGNATURE_PAYLOAD_PUSH_CERT:
+ /* Ignore payloads we don't want to parse */
+ return 0;
+ default:
+ BUG("invalid value for sigc->payload_type");
+ }
+
+ ident_line = find_commit_header(sigc->payload, signer_header, &ident_len);
+ if (!ident_line || !ident_len)
+ return 1;
+
+ if (split_ident_line(&ident, ident_line, ident_len))
+ return 1;
+
+ if (!sigc->payload_timestamp && ident.date_begin && ident.date_end)
+ sigc->payload_timestamp = parse_timestamp(ident.date_begin, NULL, 10);
+
+ return 0;
+}
+
+int check_signature(struct signature_check *sigc,
+ const char *signature, size_t slen)
{
struct gpg_format *fmt;
int status;
@@ -575,8 +623,10 @@ int check_signature(const char *payload, size_t plen, const char *signature,
if (!fmt)
die(_("bad/incompatible signature '%s'"), signature);
- status = fmt->verify_signed_buffer(sigc, fmt, payload, plen, signature,
- slen);
+ if (parse_payload_metadata(sigc))
+ return 1;
+
+ status = fmt->verify_signed_buffer(sigc, fmt, signature, slen);
if (status && !sigc->output)
return !!status;
@@ -593,7 +643,7 @@ void print_signature_buffer(const struct signature_check *sigc, unsigned flags)
sigc->output;
if (flags & GPG_VERIFY_VERBOSE && sigc->payload)
- fputs(sigc->payload, stdout);
+ fwrite(sigc->payload, 1, sigc->payload_len, stdout);
if (output)
fputs(output, stderr);
@@ -707,6 +757,21 @@ int git_gpg_config(const char *var, const char *value, void *cb)
return 0;
}
+/*
+ * Returns 1 if `string` contains a literal ssh key, 0 otherwise
+ * `key` will be set to the start of the actual key if a prefix is present.
+ */
+static int is_literal_ssh_key(const char *string, const char **key)
+{
+ if (skip_prefix(string, "key::", key))
+ return 1;
+ if (starts_with(string, "ssh-")) {
+ *key = string;
+ return 1;
+ }
+ return 0;
+}
+
static char *get_ssh_key_fingerprint(const char *signing_key)
{
struct child_process ssh_keygen = CHILD_PROCESS_INIT;
@@ -714,15 +779,16 @@ static char *get_ssh_key_fingerprint(const char *signing_key)
struct strbuf fingerprint_stdout = STRBUF_INIT;
struct strbuf **fingerprint;
char *fingerprint_ret;
+ const char *literal_key = NULL;
/*
* With SSH Signing this can contain a filename or a public key
* For textual representation we usually want a fingerprint
*/
- if (starts_with(signing_key, "ssh-")) {
+ if (is_literal_ssh_key(signing_key, &literal_key)) {
strvec_pushl(&ssh_keygen.args, "ssh-keygen", "-lf", "-", NULL);
- ret = pipe_command(&ssh_keygen, signing_key,
- strlen(signing_key), &fingerprint_stdout, 0,
+ ret = pipe_command(&ssh_keygen, literal_key,
+ strlen(literal_key), &fingerprint_stdout, 0,
NULL, 0);
} else {
strvec_pushl(&ssh_keygen.args, "ssh-keygen", "-lf",
@@ -757,6 +823,7 @@ static const char *get_default_ssh_signing_key(void)
const char **argv;
int n;
char *default_key = NULL;
+ const char *literal_key = NULL;
if (!ssh_default_key_command)
die(_("either user.signingkey or gpg.ssh.defaultKeyCommand needs to be configured"));
@@ -774,7 +841,11 @@ static const char *get_default_ssh_signing_key(void)
if (!ret) {
keys = strbuf_split_max(&key_stdout, '\n', 2);
- if (keys[0] && starts_with(keys[0]->buf, "ssh-")) {
+ if (keys[0] && is_literal_ssh_key(keys[0]->buf, &literal_key)) {
+ /*
+ * We only use `is_literal_ssh_key` here to check validity
+ * The prefix will be stripped when the key is used.
+ */
default_key = strbuf_detach(keys[0], NULL);
} else {
warning(_("gpg.ssh.defaultKeyCommand succeeded but returned no keys: %s %s"),
@@ -889,19 +960,20 @@ static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,
struct tempfile *key_file = NULL, *buffer_file = NULL;
char *ssh_signing_key_file = NULL;
struct strbuf ssh_signature_filename = STRBUF_INIT;
+ const char *literal_key = NULL;
if (!signing_key || signing_key[0] == '\0')
return error(
_("user.signingkey needs to be set for ssh signing"));
- if (starts_with(signing_key, "ssh-")) {
+ if (is_literal_ssh_key(signing_key, &literal_key)) {
/* A literal ssh key */
key_file = mks_tempfile_t(".git_signing_key_tmpXXXXXX");
if (!key_file)
return error_errno(
_("could not create temporary file"));
- keylen = strlen(signing_key);
- if (write_in_full(key_file->fd, signing_key, keylen) < 0 ||
+ keylen = strlen(literal_key);
+ if (write_in_full(key_file->fd, literal_key, keylen) < 0 ||
close_tempfile_gently(key_file) < 0) {
error_errno(_("failed writing ssh signing key to '%s'"),
key_file->filename.buf);
diff --git a/gpg-interface.h b/gpg-interface.h
index beefacbb1e..b30cbdcd3d 100644
--- a/gpg-interface.h
+++ b/gpg-interface.h
@@ -15,8 +15,18 @@ enum signature_trust_level {
TRUST_ULTIMATE,
};
+enum payload_type {
+ SIGNATURE_PAYLOAD_UNDEFINED,
+ SIGNATURE_PAYLOAD_COMMIT,
+ SIGNATURE_PAYLOAD_TAG,
+ SIGNATURE_PAYLOAD_PUSH_CERT,
+};
+
struct signature_check {
char *payload;
+ size_t payload_len;
+ enum payload_type payload_type;
+ timestamp_t payload_timestamp;
char *output;
char *gpg_status;
@@ -70,9 +80,8 @@ const char *get_signing_key(void);
* Either a GPG KeyID or a SSH Key Fingerprint
*/
const char *get_signing_key_id(void);
-int check_signature(const char *payload, size_t plen,
- const char *signature, size_t slen,
- struct signature_check *sigc);
+int check_signature(struct signature_check *sigc,
+ const char *signature, size_t slen);
void print_signature_buffer(const struct signature_check *sigc,
unsigned flags);
diff --git a/log-tree.c b/log-tree.c
index 644893fd8c..d3e7a40b64 100644
--- a/log-tree.c
+++ b/log-tree.c
@@ -513,8 +513,9 @@ static void show_signature(struct rev_info *opt, struct commit *commit)
if (parse_signed_commit(commit, &payload, &signature, the_hash_algo) <= 0)
goto out;
- status = check_signature(payload.buf, payload.len, signature.buf,
- signature.len, &sigc);
+ sigc.payload_type = SIGNATURE_PAYLOAD_COMMIT;
+ sigc.payload = strbuf_detach(&payload, &sigc.payload_len);
+ status = check_signature(&sigc, signature.buf, signature.len);
if (status && !sigc.output)
show_sig_lines(opt, status, "No signature\n");
else
@@ -583,8 +584,9 @@ static int show_one_mergetag(struct commit *commit,
status = -1;
if (parse_signature(extra->value, extra->len, &payload, &signature)) {
/* could have a good signature */
- status = check_signature(payload.buf, payload.len,
- signature.buf, signature.len, &sigc);
+ sigc.payload_type = SIGNATURE_PAYLOAD_TAG;
+ sigc.payload = strbuf_detach(&payload, &sigc.payload_len);
+ status = check_signature(&sigc, signature.buf, signature.len);
if (sigc.output)
strbuf_addstr(&verify_message, sigc.output);
else
diff --git a/repo-settings.c b/repo-settings.c
index b93e91a212..00ca5571a1 100644
--- a/repo-settings.c
+++ b/repo-settings.c
@@ -17,6 +17,9 @@ void prepare_repo_settings(struct repository *r)
char *strval;
int manyfiles;
+ if (!r->gitdir)
+ BUG("Cannot add settings for uninitialized repository");
+
if (r->settings.initialized++)
return;
diff --git a/revision.c b/revision.c
index 1981a0859f..5390a479b3 100644
--- a/revision.c
+++ b/revision.c
@@ -44,10 +44,15 @@ static inline int want_ancestry(const struct rev_info *revs);
void show_object_with_name(FILE *out, struct object *obj, const char *name)
{
- const char *p;
-
fprintf(out, "%s ", oid_to_hex(&obj->oid));
- for (p = name; *p && *p != '\n'; p++)
+ /*
+ * This "for (const char *p = ..." is made as a first step towards
+ * making use of such declarations elsewhere in our codebase. If
+ * it causes compilation problems on your platform, please report
+ * it to the Git mailing list at git@vger.kernel.org. In the meantime,
+ * adding -std=gnu99 to CFLAGS may help if you are with older GCC.
+ */
+ for (const char *p = name; *p && *p != '\n'; p++)
fputc(*p, out);
fputc('\n', out);
}
diff --git a/sequencer.c b/sequencer.c
index b69ef4dd4e..e314af4d60 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -3497,17 +3497,12 @@ static int error_failed_squash(struct repository *r,
static int do_exec(struct repository *r, const char *command_line)
{
- struct strvec child_env = STRVEC_INIT;
const char *child_argv[] = { NULL, NULL };
int dirty, status;
fprintf(stderr, _("Executing: %s\n"), command_line);
child_argv[0] = command_line;
- strvec_pushf(&child_env, "GIT_DIR=%s", absolute_path(get_git_dir()));
- strvec_pushf(&child_env, "GIT_WORK_TREE=%s",
- absolute_path(get_git_work_tree()));
- status = run_command_v_opt_cd_env(child_argv, RUN_USING_SHELL, NULL,
- child_env.v);
+ status = run_command_v_opt(child_argv, RUN_USING_SHELL);
/* force re-reading of the cache */
if (discard_index(r->index) < 0 || repo_read_index(r) < 0)
@@ -3537,8 +3532,6 @@ static int do_exec(struct repository *r, const char *command_line)
status = 1;
}
- strvec_clear(&child_env);
-
return status;
}
diff --git a/t/Makefile b/t/Makefile
index 882d26eee3..46cd5fc527 100644
--- a/t/Makefile
+++ b/t/Makefile
@@ -71,12 +71,10 @@ clean-chainlint:
check-chainlint:
@mkdir -p '$(CHAINLINTTMP_SQ)' && \
- err=0 && \
- for i in $(CHAINLINTTESTS); do \
- $(CHAINLINT) <chainlint/$$i.test | \
- sed -e '/^# LINT: /d' >'$(CHAINLINTTMP_SQ)'/$$i.actual && \
- diff -u chainlint/$$i.expect '$(CHAINLINTTMP_SQ)'/$$i.actual || err=1; \
- done && exit $$err
+ sed -e '/^# LINT: /d' $(patsubst %,chainlint/%.test,$(CHAINLINTTESTS)) >'$(CHAINLINTTMP_SQ)'/tests && \
+ sed -e '/^[ ]*$$/d' $(patsubst %,chainlint/%.expect,$(CHAINLINTTESTS)) >'$(CHAINLINTTMP_SQ)'/expect && \
+ $(CHAINLINT) '$(CHAINLINTTMP_SQ)'/tests | grep -v '^[ ]*$$' >'$(CHAINLINTTMP_SQ)'/actual && \
+ diff -u '$(CHAINLINTTMP_SQ)'/expect '$(CHAINLINTTMP_SQ)'/actual
test-lint: test-lint-duplicates test-lint-executable test-lint-shell-syntax \
test-lint-filenames
diff --git a/t/chainlint.sed b/t/chainlint.sed
index 8a25c5b855..dc4ce37cb5 100644
--- a/t/chainlint.sed
+++ b/t/chainlint.sed
@@ -24,9 +24,9 @@
# in order to avoid misinterpreting the ")" in constructs such as "x=$(...)"
# and "case $x in *)" as ending the subshell.
#
-# Lines missing a final "&&" are flagged with "?!AMP?!", and lines which chain
-# commands with ";" internally rather than "&&" are flagged "?!SEMI?!". A line
-# may be flagged for both violations.
+# Lines missing a final "&&" are flagged with "?!AMP?!", as are lines which
+# chain commands with ";" internally rather than "&&". A line may be flagged
+# for both violations.
#
# Detection of a missing &&-link in a multi-line subshell is complicated by the
# fact that the last statement before the closing ")" must not end with "&&".
@@ -47,8 +47,8 @@
# "?!AMP?!" violation is removed from the "bar" line (retrieved from the "hold"
# area) since the final statement of a subshell must not end with "&&". The
# final line of a subshell may still break the &&-chain by using ";" internally
-# to chain commands together rather than "&&", so "?!SEMI?!" is never removed
-# from a line (even though "?!AMP?!" might be).
+# to chain commands together rather than "&&", but an internal "?!AMP?!" is
+# never removed from a line even though a line-ending "?!AMP?!" might be.
#
# Care is taken to recognize the last _statement_ of a multi-line subshell, not
# necessarily the last textual _line_ within the subshell, since &&-chaining
@@ -62,26 +62,20 @@
# receives similar treatment.
#
# Swallowing here-docs with arbitrary tags requires a bit of finesse. When a
-# line such as "cat <<EOF >out" is seen, the here-doc tag is moved to the front
-# of the line enclosed in angle brackets as a sentinel, giving "<EOF>cat >out".
+# line such as "cat <<EOF" is seen, the here-doc tag is copied to the front of
+# the line enclosed in angle brackets as a sentinel, giving "<EOF>cat <<EOF".
# As each subsequent line is read, it is appended to the target line and a
# (whitespace-loose) back-reference match /^<(.*)>\n\1$/ is attempted to see if
# the content inside "<...>" matches the entirety of the newly-read line. For
# instance, if the next line read is "some data", when concatenated with the
-# target line, it becomes "<EOF>cat >out\nsome data", and a match is attempted
+# target line, it becomes "<EOF>cat <<EOF\nsome data", and a match is attempted
# to see if "EOF" matches "some data". Since it doesn't, the next line is
# attempted. When a line consisting of only "EOF" (and possible whitespace) is
-# encountered, it is appended to the target line giving "<EOF>cat >out\nEOF",
+# encountered, it is appended to the target line giving "<EOF>cat <<EOF\nEOF",
# in which case the "EOF" inside "<...>" does match the text following the
# newline, thus the closing here-doc tag has been found. The closing tag line
# and the "<...>" prefix on the target line are then discarded, leaving just
-# the target line "cat >out".
-#
-# To facilitate regression testing (and manual debugging), a ">" annotation is
-# applied to the line containing ")" which closes a subshell, ">>" to a line
-# closing a nested subshell, and ">>>" to a line closing both at once. This
-# makes it easy to detect whether the heuristics correctly identify
-# end-of-subshell.
+# the target line "cat <<EOF".
#------------------------------------------------------------------------------
# incomplete line -- slurp up next line
@@ -94,9 +88,9 @@
# here-doc -- swallow it to avoid false hits within its body (but keep the
# command to which it was attached)
-/<<[ ]*[-\\'"]*[A-Za-z0-9_]/ {
- s/^\(.*\)<<[ ]*[-\\'"]*\([A-Za-z0-9_][A-Za-z0-9_]*\)['"]*/<\2>\1<</
- s/[ ]*<<//
+/<<-*[ ]*[\\'"]*[A-Za-z0-9_]/ {
+ /"[^"]*<<[^"]*"/bnotdoc
+ s/^\(.*<<-*[ ]*\)[\\'"]*\([A-Za-z0-9_][A-Za-z0-9_]*\)['"]*/<\2>\1\2/
:hered
N
/^<\([^>]*\)>.*\n[ ]*\1[ ]*$/!{
@@ -106,6 +100,7 @@
s/^<[^>]*>//
s/\n.*$//
}
+:notdoc
# one-liner "(...) &&"
/^[ ]*!*[ ]*(..*)[ ]*&&[ ]*$/boneline
@@ -126,7 +121,7 @@ b
# "&&" (but not ";" in a string)
:oneline
/;/{
- /"[^"]*;[^"]*"/!s/^/?!SEMI?!/
+ /"[^"]*;[^"]*"/!s/;/; ?!AMP?!/
}
b
@@ -136,11 +131,15 @@ b
h
bnextln
}
-# "(..." line -- split off and stash "(", then process "..." as its own line
+# "(..." line -- "(" opening subshell cuddled with command; temporarily replace
+# "(" with sentinel "^" and process the line as if "(" had been seen solo on
+# the preceding line; this temporary replacement prevents several rules from
+# accidentally thinking "(" introduces a nested subshell; "^" is changed back
+# to "(" at output time
x
-s/.*/(/
+s/.*//
x
-s/(//
+s/(/^/
bslurp
:nextln
@@ -157,8 +156,10 @@ s/.*\n//
/"[^'"]*'[^'"]*"/!bsqstr
}
:folded
-# here-doc -- swallow it
-/<<[ ]*[-\\'"]*[A-Za-z0-9_]/bheredoc
+# here-doc -- swallow it (but not "<<" in a string)
+/<<-*[ ]*[\\'"]*[A-Za-z0-9_]/{
+ /"[^"]*<<[^"]*"/!bheredoc
+}
# comment or empty line -- discard since final non-comment, non-empty line
# before closing ")", "done", "elsif", "else", or "fi" will need to be
# re-visited to drop "suspect" marking since final line of those constructs
@@ -171,12 +172,12 @@ s/.*\n//
/"[^"]*#[^"]*"/!s/[ ]#.*$//
}
# one-liner "case ... esac"
-/^[ ]*case[ ]*..*esac/bchkchn
+/^[ ^]*case[ ]*..*esac/bchkchn
# multi-line "case ... esac"
-/^[ ]*case[ ]..*[ ]in/bcase
+/^[ ^]*case[ ]..*[ ]in/bcase
# multi-line "for ... done" or "while ... done"
-/^[ ]*for[ ]..*[ ]in/bcont
-/^[ ]*while[ ]/bcont
+/^[ ^]*for[ ]..*[ ]in/bcont
+/^[ ^]*while[ ]/bcont
/^[ ]*do[ ]/bcont
/^[ ]*do[ ]*$/bcont
/;[ ]*do/bcont
@@ -187,7 +188,7 @@ s/.*\n//
/||[ ]*exit[ ]/bcont
/||[ ]*exit[ ]*$/bcont
# multi-line "if...elsif...else...fi"
-/^[ ]*if[ ]/bcont
+/^[ ^]*if[ ]/bcont
/^[ ]*then[ ]/bcont
/^[ ]*then[ ]*$/bcont
/;[ ]*then/bcont
@@ -200,15 +201,15 @@ s/.*\n//
/^[ ]*fi[ ]*[<>|]/bdone
/^[ ]*fi[ ]*)/bdone
# nested one-liner "(...) &&"
-/^[ ]*(.*)[ ]*&&[ ]*$/bchkchn
+/^[ ^]*(.*)[ ]*&&[ ]*$/bchkchn
# nested one-liner "(...)"
-/^[ ]*(.*)[ ]*$/bchkchn
+/^[ ^]*(.*)[ ]*$/bchkchn
# nested one-liner "(...) >x" (or "2>x" or "<x" or "|x")
-/^[ ]*(.*)[ ]*[0-9]*[<>|]/bchkchn
+/^[ ^]*(.*)[ ]*[0-9]*[<>|]/bchkchn
# nested multi-line "(...\n...)"
-/^[ ]*(/bnest
+/^[ ^]*(/bnest
# multi-line "{...\n...}"
-/^[ ]*{/bblock
+/^[ ^]*{/bblock
# closing ")" on own line -- exit subshell
/^[ ]*)/bclssolo
# "$((...))" -- arithmetic expansion; not closing ")"
@@ -230,16 +231,18 @@ s/.*\n//
# string and not ";;" in one-liner "case...esac")
/;/{
/;;/!{
- /"[^"]*;[^"]*"/!s/^/?!SEMI?!/
+ /"[^"]*;[^"]*"/!s/;/; ?!AMP?!/
}
}
# line ends with pipe "...|" -- valid; not missing "&&"
/|[ ]*$/bcont
# missing end-of-line "&&" -- mark suspect
-/&&[ ]*$/!s/^/?!AMP?!/
+/&&[ ]*$/!s/$/ ?!AMP?!/
:cont
# retrieve and print previous line
x
+s/^\([ ]*\)^/\1(/
+s/?!HERE?!/<</g
n
bslurp
@@ -280,8 +283,7 @@ bfolded
# found here-doc -- swallow it to avoid false hits within its body (but keep
# the command to which it was attached)
:heredoc
-s/^\(.*\)<<[ ]*[-\\'"]*\([A-Za-z0-9_][A-Za-z0-9_]*\)['"]*/<\2>\1<</
-s/[ ]*<<//
+s/^\(.*\)<<\(-*[ ]*\)[\\'"]*\([A-Za-z0-9_][A-Za-z0-9_]*\)['"]*/<\3>\1?!HERE?!\2\3/
:hdocsub
N
/^<\([^>]*\)>.*\n[ ]*\1[ ]*$/!{
@@ -295,7 +297,15 @@ bfolded
# found "case ... in" -- pass through untouched
:case
x
+s/^\([ ]*\)^/\1(/
+s/?!HERE?!/<</g
n
+:cascom
+/^[ ]*#/{
+ N
+ s/.*\n//
+ bcascom
+}
/^[ ]*esac/bslurp
bcase
@@ -303,7 +313,7 @@ bcase
# that line legitimately lacks "&&"
:else
x
-s/?!AMP?!//
+s/\( ?!AMP?!\)* ?!AMP?!$//
x
bcont
@@ -311,7 +321,7 @@ bcont
# "suspect" from final contained line since that line legitimately lacks "&&"
:done
x
-s/?!AMP?!//
+s/\( ?!AMP?!\)* ?!AMP?!$//
x
# is 'done' or 'fi' cuddled with ")" to close subshell?
/done.*)/bclose
@@ -322,11 +332,18 @@ bchkchn
:nest
x
:nstslrp
+s/^\([ ]*\)^/\1(/
+s/?!HERE?!/<</g
n
+:nstcom
+# comment -- not closing ")" if in comment
+/^[ ]*#/{
+ N
+ s/.*\n//
+ bnstcom
+}
# closing ")" on own line -- stop nested slurp
/^[ ]*)/bnstcl
-# comment -- not closing ")" if in comment
-/^[ ]*#/bnstcnt
# "$((...))" -- arithmetic expansion; not closing ")"
/\$(([^)][^)]*))[^)]*$/bnstcnt
# "$(...)" -- command substitution; not closing ")"
@@ -337,7 +354,6 @@ n
x
bnstslrp
:nstcl
-s/^/>>/
# is it "))" which closes nested and parent subshells?
/)[ ]*)/bslurp
bchkchn
@@ -345,7 +361,15 @@ bchkchn
# found multi-line "{...\n...}" block -- pass through untouched
:block
x
+s/^\([ ]*\)^/\1(/
+s/?!HERE?!/<</g
n
+:blkcom
+/^[ ]*#/{
+ N
+ s/.*\n//
+ bblkcom
+}
# closing "}" -- stop block slurp
/}/bchkchn
bblock
@@ -354,16 +378,22 @@ bblock
# since that line legitimately lacks "&&" and exit subshell loop
:clssolo
x
-s/?!AMP?!//
+s/\( ?!AMP?!\)* ?!AMP?!$//
+s/^\([ ]*\)^/\1(/
+s/?!HERE?!/<</g
p
x
-s/^/>/
+s/^\([ ]*\)^/\1(/
+s/?!HERE?!/<</g
b
# found closing "...)" -- exit subshell loop
:close
x
+s/^\([ ]*\)^/\1(/
+s/?!HERE?!/<</g
p
x
-s/^/>/
+s/^\([ ]*\)^/\1(/
+s/?!HERE?!/<</g
b
diff --git a/t/chainlint/arithmetic-expansion.expect b/t/chainlint/arithmetic-expansion.expect
index 09457d3196..46ee1046af 100644
--- a/t/chainlint/arithmetic-expansion.expect
+++ b/t/chainlint/arithmetic-expansion.expect
@@ -2,8 +2,8 @@
foo &&
bar=$((42 + 1)) &&
baz
->) &&
+) &&
(
-?!AMP?! bar=$((42 + 1))
+ bar=$((42 + 1)) ?!AMP?!
baz
->)
+)
diff --git a/t/chainlint/bash-array.expect b/t/chainlint/bash-array.expect
index c4a830d1c1..4c34eaee45 100644
--- a/t/chainlint/bash-array.expect
+++ b/t/chainlint/bash-array.expect
@@ -2,9 +2,9 @@
foo &&
bar=(gumbo stumbo wumbo) &&
baz
->) &&
+) &&
(
foo &&
bar=${#bar[@]} &&
baz
->)
+)
diff --git a/t/chainlint/blank-line.expect b/t/chainlint/blank-line.expect
index 3be939ed38..f76fde1ffb 100644
--- a/t/chainlint/blank-line.expect
+++ b/t/chainlint/blank-line.expect
@@ -1,4 +1,4 @@
(
nothing &&
something
->)
+)
diff --git a/t/chainlint/blank-line.test b/t/chainlint/blank-line.test
index f6dd14302b..0fdf15b3e1 100644
--- a/t/chainlint/blank-line.test
+++ b/t/chainlint/blank-line.test
@@ -3,7 +3,7 @@
nothing &&
something
-# LINT: swallow blank lines since final _statement_ before subshell end is
+# LINT: ignore blank lines since final _statement_ before subshell end is
# LINT: significant to "&&"-check, not final _line_ (which might be blank)
diff --git a/t/chainlint/block-comment.expect b/t/chainlint/block-comment.expect
new file mode 100644
index 0000000000..d10b2eeaf2
--- /dev/null
+++ b/t/chainlint/block-comment.expect
@@ -0,0 +1,6 @@
+(
+ {
+ echo a &&
+ echo b
+ }
+)
diff --git a/t/chainlint/block-comment.test b/t/chainlint/block-comment.test
new file mode 100644
index 0000000000..df2beea888
--- /dev/null
+++ b/t/chainlint/block-comment.test
@@ -0,0 +1,8 @@
+(
+ {
+ # show a
+ echo a &&
+ # show b
+ echo b
+ }
+)
diff --git a/t/chainlint/block.expect b/t/chainlint/block.expect
index fed7e89ae8..da60257ebc 100644
--- a/t/chainlint/block.expect
+++ b/t/chainlint/block.expect
@@ -7,6 +7,6 @@
bar &&
{
echo c
-?!AMP?! }
+ } ?!AMP?!
baz
->)
+)
diff --git a/t/chainlint/block.test b/t/chainlint/block.test
index d859151af1..0a82fd579f 100644
--- a/t/chainlint/block.test
+++ b/t/chainlint/block.test
@@ -1,6 +1,5 @@
(
-# LINT: missing "&&" in block not currently detected (for consistency with
-# LINT: --chain-lint at top level and to provide escape hatch if needed)
+# LINT: missing "&&" after first "echo"
foo &&
{
echo a
diff --git a/t/chainlint/broken-chain.expect b/t/chainlint/broken-chain.expect
index 55b0f42a53..cfb58fb6b9 100644
--- a/t/chainlint/broken-chain.expect
+++ b/t/chainlint/broken-chain.expect
@@ -1,6 +1,6 @@
(
foo &&
-?!AMP?! bar
+ bar ?!AMP?!
baz &&
wop
->)
+)
diff --git a/t/chainlint/broken-chain.test b/t/chainlint/broken-chain.test
index 3cc67b65d0..2a44aa73b7 100644
--- a/t/chainlint/broken-chain.test
+++ b/t/chainlint/broken-chain.test
@@ -1,6 +1,6 @@
(
foo &&
-# LINT: missing "&&" from 'bar'
+# LINT: missing "&&" from "bar"
bar
baz &&
# LINT: final statement before closing ")" legitimately lacks "&&"
diff --git a/t/chainlint/case-comment.expect b/t/chainlint/case-comment.expect
new file mode 100644
index 0000000000..1e4b054bda
--- /dev/null
+++ b/t/chainlint/case-comment.expect
@@ -0,0 +1,8 @@
+(
+ case "$x" in
+ x) foo ;;
+ *)
+ bar
+ ;;
+ esac
+)
diff --git a/t/chainlint/case-comment.test b/t/chainlint/case-comment.test
new file mode 100644
index 0000000000..641c157b98
--- /dev/null
+++ b/t/chainlint/case-comment.test
@@ -0,0 +1,11 @@
+(
+ case "$x" in
+ # found foo
+ x) foo ;;
+ # found other
+ *)
+ # treat it as bar
+ bar
+ ;;
+ esac
+)
diff --git a/t/chainlint/case.expect b/t/chainlint/case.expect
index 41f121fbbf..31f280d8ce 100644
--- a/t/chainlint/case.expect
+++ b/t/chainlint/case.expect
@@ -4,16 +4,16 @@
*) bar ;;
esac &&
foobar
->) &&
+) &&
(
case "$x" in
x) foo ;;
*) bar ;;
-?!AMP?! esac
+ esac ?!AMP?!
foobar
->) &&
+) &&
(
case "$x" in 1) true;; esac &&
-?!AMP?! case "$y" in 2) false;; esac
+ case "$y" in 2) false;; esac ?!AMP?!
foobar
->)
+)
diff --git a/t/chainlint/case.test b/t/chainlint/case.test
index 5ef6ff7db5..4cb086bf87 100644
--- a/t/chainlint/case.test
+++ b/t/chainlint/case.test
@@ -1,5 +1,5 @@
(
-# LINT: "...)" arms in 'case' not misinterpreted as subshell-closing ")"
+# LINT: "...)" arms in "case" not misinterpreted as subshell-closing ")"
case "$x" in
x) foo ;;
*) bar ;;
@@ -7,7 +7,7 @@
foobar
) &&
(
-# LINT: missing "&&" on 'esac'
+# LINT: missing "&&" on "esac"
case "$x" in
x) foo ;;
*) bar ;;
@@ -15,7 +15,7 @@
foobar
) &&
(
-# LINT: "...)" arm in one-liner 'case' not misinterpreted as closing ")"
+# LINT: "...)" arm in one-liner "case" not misinterpreted as closing ")"
case "$x" in 1) true;; esac &&
# LINT: same but missing "&&"
case "$y" in 2) false;; esac
diff --git a/t/chainlint/close-nested-and-parent-together.expect b/t/chainlint/close-nested-and-parent-together.expect
index 2a910f9d66..72d482f76d 100644
--- a/t/chainlint/close-nested-and-parent-together.expect
+++ b/t/chainlint/close-nested-and-parent-together.expect
@@ -1,4 +1,3 @@
-(
-cd foo &&
+(cd foo &&
(bar &&
->>> baz))
+ baz))
diff --git a/t/chainlint/close-subshell.expect b/t/chainlint/close-subshell.expect
index 184688718a..0f87db9ae6 100644
--- a/t/chainlint/close-subshell.expect
+++ b/t/chainlint/close-subshell.expect
@@ -1,25 +1,25 @@
(
foo
->) &&
+) &&
(
bar
->) >out &&
+) >out &&
(
baz
->) 2>err &&
+) 2>err &&
(
boo
->) <input &&
+) <input &&
(
bip
->) | wuzzle &&
+) | wuzzle &&
(
bop
->) | fazz fozz &&
+) | fazz fozz &&
(
bup
->) |
+) |
fuzzle &&
(
yop
->)
+)
diff --git a/t/chainlint/command-substitution.expect b/t/chainlint/command-substitution.expect
index ad4118e537..c72e4df9e7 100644
--- a/t/chainlint/command-substitution.expect
+++ b/t/chainlint/command-substitution.expect
@@ -2,8 +2,8 @@
foo &&
bar=$(gobble) &&
baz
->) &&
+) &&
(
-?!AMP?! bar=$(gobble blocks)
+ bar=$(gobble blocks) ?!AMP?!
baz
->)
+)
diff --git a/t/chainlint/comment.expect b/t/chainlint/comment.expect
index 3be939ed38..f76fde1ffb 100644
--- a/t/chainlint/comment.expect
+++ b/t/chainlint/comment.expect
@@ -1,4 +1,4 @@
(
nothing &&
something
->)
+)
diff --git a/t/chainlint/complex-if-in-cuddled-loop.expect b/t/chainlint/complex-if-in-cuddled-loop.expect
index 9674b88cf2..2fca183409 100644
--- a/t/chainlint/complex-if-in-cuddled-loop.expect
+++ b/t/chainlint/complex-if-in-cuddled-loop.expect
@@ -1,10 +1,9 @@
-(
-for i in a b c; do
+(for i in a b c; do
if test "$(echo $(waffle bat))" = "eleventeen" &&
test "$x" = "$y"; then
:
else
echo >file
fi
-> done) &&
+ done) &&
test ! -f file
diff --git a/t/chainlint/complex-if-in-cuddled-loop.test b/t/chainlint/complex-if-in-cuddled-loop.test
index 571bbd85cd..5efeda58b2 100644
--- a/t/chainlint/complex-if-in-cuddled-loop.test
+++ b/t/chainlint/complex-if-in-cuddled-loop.test
@@ -1,4 +1,4 @@
-# LINT: 'for' loop cuddled with "(" and ")" and nested 'if' with complex
+# LINT: "for" loop cuddled with "(" and ")" and nested "if" with complex
# LINT: multi-line condition; indented with spaces, not tabs
(for i in a b c; do
if test "$(echo $(waffle bat))" = "eleventeen" &&
diff --git a/t/chainlint/cuddled-if-then-else.expect b/t/chainlint/cuddled-if-then-else.expect
index ab2a026fbc..1d8ed58c49 100644
--- a/t/chainlint/cuddled-if-then-else.expect
+++ b/t/chainlint/cuddled-if-then-else.expect
@@ -1,7 +1,6 @@
-(
-if test -z ""; then
+(if test -z ""; then
echo empty
else
echo bizzy
-> fi) &&
+ fi) &&
echo foobar
diff --git a/t/chainlint/cuddled-if-then-else.test b/t/chainlint/cuddled-if-then-else.test
index eed774a9d6..7c53f4efe3 100644
--- a/t/chainlint/cuddled-if-then-else.test
+++ b/t/chainlint/cuddled-if-then-else.test
@@ -1,4 +1,4 @@
-# LINT: 'if' cuddled with "(" and ")"; indented with spaces, not tabs
+# LINT: "if" cuddled with "(" and ")"; indented with spaces, not tabs
(if test -z ""; then
echo empty
else
diff --git a/t/chainlint/cuddled-loop.expect b/t/chainlint/cuddled-loop.expect
index 8c0260d7f1..9cf260708e 100644
--- a/t/chainlint/cuddled-loop.expect
+++ b/t/chainlint/cuddled-loop.expect
@@ -1,5 +1,4 @@
-(
- while read x
+( while read x
do foobar bop || exit 1
-> done <file ) &&
+ done <file ) &&
outside subshell
diff --git a/t/chainlint/cuddled-loop.test b/t/chainlint/cuddled-loop.test
index a841d781f0..3c2a62f751 100644
--- a/t/chainlint/cuddled-loop.test
+++ b/t/chainlint/cuddled-loop.test
@@ -1,4 +1,4 @@
-# LINT: 'while' loop cuddled with "(" and ")", with embedded (allowed)
+# LINT: "while" loop cuddled with "(" and ")", with embedded (allowed)
# LINT: "|| exit {n}" to exit loop early, and using redirection "<" to feed
# LINT: loop; indented with spaces, not tabs
( while read x
diff --git a/t/chainlint/cuddled.expect b/t/chainlint/cuddled.expect
index b506d46221..c3e0be4047 100644
--- a/t/chainlint/cuddled.expect
+++ b/t/chainlint/cuddled.expect
@@ -1,21 +1,17 @@
-(
-cd foo &&
+(cd foo &&
bar
->) &&
+) &&
-(
-?!AMP?!cd foo
+(cd foo ?!AMP?!
bar
->) &&
+) &&
(
cd foo &&
-> bar) &&
+ bar) &&
-(
-cd foo &&
-> bar) &&
+(cd foo &&
+ bar) &&
-(
-?!AMP?!cd foo
-> bar)
+(cd foo ?!AMP?!
+ bar)
diff --git a/t/chainlint/cuddled.test b/t/chainlint/cuddled.test
index 0499fa4180..257b5b5eed 100644
--- a/t/chainlint/cuddled.test
+++ b/t/chainlint/cuddled.test
@@ -1,5 +1,4 @@
-# LINT: first subshell statement cuddled with opening "("; for implementation
-# LINT: simplicity, "(..." is split into two lines, "(" and "..."
+# LINT: first subshell statement cuddled with opening "("
(cd foo &&
bar
) &&
diff --git a/t/chainlint/exit-loop.expect b/t/chainlint/exit-loop.expect
index 84d8bdebc0..f76aa60466 100644
--- a/t/chainlint/exit-loop.expect
+++ b/t/chainlint/exit-loop.expect
@@ -5,7 +5,7 @@
bar &&
baz
done
->) &&
+) &&
(
while true
do
@@ -13,7 +13,7 @@
bar &&
baz
done
->) &&
+) &&
(
i=0 &&
while test $i -lt 10
@@ -21,4 +21,4 @@
echo $i || exit
i=$(($i + 1))
done
->)
+)
diff --git a/t/chainlint/exit-subshell.expect b/t/chainlint/exit-subshell.expect
index bf78454f74..da80339f78 100644
--- a/t/chainlint/exit-subshell.expect
+++ b/t/chainlint/exit-subshell.expect
@@ -2,4 +2,4 @@
foo || exit 1
bar &&
baz
->)
+)
diff --git a/t/chainlint/for-loop.expect b/t/chainlint/for-loop.expect
index c33cf56ee7..6671b8cd84 100644
--- a/t/chainlint/for-loop.expect
+++ b/t/chainlint/for-loop.expect
@@ -1,11 +1,11 @@
(
for i in a b c
do
-?!AMP?! echo $i
- cat
-?!AMP?! done
+ echo $i ?!AMP?!
+ cat <<-EOF
+ done ?!AMP?!
for i in a b c; do
echo $i &&
cat $i
done
->)
+)
diff --git a/t/chainlint/for-loop.test b/t/chainlint/for-loop.test
index 7db76262bc..6cb3428158 100644
--- a/t/chainlint/for-loop.test
+++ b/t/chainlint/for-loop.test
@@ -1,17 +1,17 @@
(
-# LINT: 'for', 'do', 'done' do not need "&&"
+# LINT: "for", "do", "done" do not need "&&"
for i in a b c
do
-# LINT: missing "&&" on 'echo'
+# LINT: missing "&&" on "echo"
echo $i
# LINT: last statement of while does not need "&&"
cat <<-\EOF
bar
EOF
-# LINT: missing "&&" on 'done'
+# LINT: missing "&&" on "done"
done
-# LINT: 'do' on same line as 'for'
+# LINT: "do" on same line as "for"
for i in a b c; do
echo $i &&
cat $i
diff --git a/t/chainlint/here-doc-close-subshell.expect b/t/chainlint/here-doc-close-subshell.expect
index f011e335e5..2af9ced71c 100644
--- a/t/chainlint/here-doc-close-subshell.expect
+++ b/t/chainlint/here-doc-close-subshell.expect
@@ -1,2 +1,2 @@
(
-> cat)
+ cat <<-INPUT)
diff --git a/t/chainlint/here-doc-multi-line-command-subst.expect b/t/chainlint/here-doc-multi-line-command-subst.expect
index e5fb752d2f..f8b3aa73c4 100644
--- a/t/chainlint/here-doc-multi-line-command-subst.expect
+++ b/t/chainlint/here-doc-multi-line-command-subst.expect
@@ -1,5 +1,5 @@
(
- x=$(bobble &&
-?!AMP?!>> wiffle)
+ x=$(bobble <<-END &&
+ wiffle) ?!AMP?!
echo $x
->)
+)
diff --git a/t/chainlint/here-doc-multi-line-string.expect b/t/chainlint/here-doc-multi-line-string.expect
index 32038a070c..2578191ca8 100644
--- a/t/chainlint/here-doc-multi-line-string.expect
+++ b/t/chainlint/here-doc-multi-line-string.expect
@@ -1,4 +1,4 @@
(
-?!AMP?! cat && echo "multi-line string"
+ cat <<-TXT && echo "multi-line string" ?!AMP?!
bap
->)
+)
diff --git a/t/chainlint/here-doc.expect b/t/chainlint/here-doc.expect
index 534b065e38..110059ba58 100644
--- a/t/chainlint/here-doc.expect
+++ b/t/chainlint/here-doc.expect
@@ -1,9 +1,7 @@
-boodle wobba gorgo snoot wafta snurb &&
+boodle wobba gorgo snoot wafta snurb <<EOF &&
-cat >foo &&
+cat <<-Arbitrary_Tag_42 >foo &&
-cat >bar &&
+cat <<zump >boo &&
-cat >boo &&
-
-horticulture
+horticulture <<EOF
diff --git a/t/chainlint/here-doc.test b/t/chainlint/here-doc.test
index ad4ce8afd9..3f5f92cad3 100644
--- a/t/chainlint/here-doc.test
+++ b/t/chainlint/here-doc.test
@@ -14,13 +14,6 @@ boz
woz
Arbitrary_Tag_42
-# LINT: swallow 'quoted' here-doc
-cat <<'FUMP' >bar &&
-snoz
-boz
-woz
-FUMP
-
# LINT: swallow "quoted" here-doc
cat <<"zump" >boo &&
snoz
diff --git a/t/chainlint/if-in-loop.expect b/t/chainlint/if-in-loop.expect
index 03d3ceb22d..03b82a3e58 100644
--- a/t/chainlint/if-in-loop.expect
+++ b/t/chainlint/if-in-loop.expect
@@ -3,10 +3,10 @@
do
if false
then
-?!AMP?! echo "err"
+ echo "err" ?!AMP?!
exit 1
-?!AMP?! fi
+ fi ?!AMP?!
foo
-?!AMP?! done
+ done ?!AMP?!
bar
->)
+)
diff --git a/t/chainlint/if-in-loop.test b/t/chainlint/if-in-loop.test
index daf22da164..f0cf19cfad 100644
--- a/t/chainlint/if-in-loop.test
+++ b/t/chainlint/if-in-loop.test
@@ -3,13 +3,13 @@
do
if false
then
-# LINT: missing "&&" on 'echo'
+# LINT: missing "&&" on "echo"
echo "err"
exit 1
-# LINT: missing "&&" on 'fi'
+# LINT: missing "&&" on "fi"
fi
foo
-# LINT: missing "&&" on 'done'
+# LINT: missing "&&" on "done"
done
bar
)
diff --git a/t/chainlint/if-then-else.expect b/t/chainlint/if-then-else.expect
index 5953c7bfbc..44d86c3597 100644
--- a/t/chainlint/if-then-else.expect
+++ b/t/chainlint/if-then-else.expect
@@ -1,19 +1,20 @@
(
if test -n ""
then
-?!AMP?! echo very
+ echo very ?!AMP?!
echo empty
elif test -z ""
+ then
echo foo
else
echo foo &&
- cat
-?!AMP?! fi
+ cat <<-EOF
+ fi ?!AMP?!
echo poodle
->) &&
+) &&
(
if test -n ""; then
echo very &&
-?!AMP?! echo empty
- if
->)
+ echo empty
+ fi
+)
diff --git a/t/chainlint/if-then-else.test b/t/chainlint/if-then-else.test
index 9bd8e9a4c6..2055336c2b 100644
--- a/t/chainlint/if-then-else.test
+++ b/t/chainlint/if-then-else.test
@@ -1,28 +1,29 @@
(
-# LINT: 'if', 'then', 'elif', 'else', 'fi' do not need "&&"
+# LINT: "if", "then", "elif", "else", "fi" do not need "&&"
if test -n ""
then
-# LINT: missing "&&" on 'echo'
+# LINT: missing "&&" on "echo"
echo very
-# LINT: last statement before 'elif' does not need "&&"
+# LINT: last statement before "elif" does not need "&&"
echo empty
elif test -z ""
-# LINT: last statement before 'else' does not need "&&"
+ then
+# LINT: last statement before "else" does not need "&&"
echo foo
else
echo foo &&
-# LINT: last statement before 'fi' does not need "&&"
+# LINT: last statement before "fi" does not need "&&"
cat <<-\EOF
bar
EOF
-# LINT: missing "&&" on 'fi'
+# LINT: missing "&&" on "fi"
fi
echo poodle
) &&
(
-# LINT: 'then' on same line as 'if'
+# LINT: "then" on same line as "if"
if test -n ""; then
echo very &&
echo empty
- if
+ fi
)
diff --git a/t/chainlint/incomplete-line.expect b/t/chainlint/incomplete-line.expect
index 2f3ebabdc2..ffac8f9018 100644
--- a/t/chainlint/incomplete-line.expect
+++ b/t/chainlint/incomplete-line.expect
@@ -1,4 +1,4 @@
line 1 line 2 line 3 line 4 &&
(
line 5 line 6 line 7 line 8
->)
+)
diff --git a/t/chainlint/inline-comment.expect b/t/chainlint/inline-comment.expect
index fc9f250ac4..dd0dace077 100644
--- a/t/chainlint/inline-comment.expect
+++ b/t/chainlint/inline-comment.expect
@@ -1,9 +1,8 @@
(
foobar &&
-?!AMP?! barfoo
+ barfoo ?!AMP?!
flibble "not a # comment"
->) &&
+) &&
-(
-cd foo &&
-> flibble "not a # comment")
+(cd foo &&
+ flibble "not a # comment")
diff --git a/t/chainlint/loop-in-if.expect b/t/chainlint/loop-in-if.expect
index 088e622c31..e1be42376c 100644
--- a/t/chainlint/loop-in-if.expect
+++ b/t/chainlint/loop-in-if.expect
@@ -3,10 +3,10 @@
then
while true
do
-?!AMP?! echo "pop"
+ echo "pop" ?!AMP?!
echo "glup"
-?!AMP?! done
+ done ?!AMP?!
foo
-?!AMP?! fi
+ fi ?!AMP?!
bar
->)
+)
diff --git a/t/chainlint/loop-in-if.test b/t/chainlint/loop-in-if.test
index 93e8ba8e4d..dfcc3f98fb 100644
--- a/t/chainlint/loop-in-if.test
+++ b/t/chainlint/loop-in-if.test
@@ -3,13 +3,13 @@
then
while true
do
-# LINT: missing "&&" on 'echo'
+# LINT: missing "&&" on "echo"
echo "pop"
echo "glup"
-# LINT: missing "&&" on 'done'
+# LINT: missing "&&" on "done"
done
foo
-# LINT: missing "&&" on 'fi'
+# LINT: missing "&&" on "fi"
fi
bar
)
diff --git a/t/chainlint/multi-line-nested-command-substitution.expect b/t/chainlint/multi-line-nested-command-substitution.expect
index 59b6c8b850..300058341b 100644
--- a/t/chainlint/multi-line-nested-command-substitution.expect
+++ b/t/chainlint/multi-line-nested-command-substitution.expect
@@ -3,16 +3,16 @@
x=$(
echo bar |
cat
->> ) &&
+ ) &&
echo ok
->) |
+) |
sort &&
(
bar &&
x=$(echo bar |
cat
->> ) &&
+ ) &&
y=$(echo baz |
->> fip) &&
+ fip) &&
echo fail
->)
+)
diff --git a/t/chainlint/multi-line-string.expect b/t/chainlint/multi-line-string.expect
index 170cb59993..ab0dadf748 100644
--- a/t/chainlint/multi-line-string.expect
+++ b/t/chainlint/multi-line-string.expect
@@ -1,15 +1,9 @@
(
x="line 1 line 2 line 3" &&
-?!AMP?! y='line 1 line2'
+ y="line 1 line2" ?!AMP?!
foobar
->) &&
-(
- echo "there's nothing to see here" &&
- exit
->) &&
+) &&
(
echo "xyz" "abc def ghi" &&
- echo 'xyz' 'abc def ghi' &&
- echo 'xyz' "abc def ghi" &&
barfoo
->)
+)
diff --git a/t/chainlint/multi-line-string.test b/t/chainlint/multi-line-string.test
index 287ab89705..4a0af2107d 100644
--- a/t/chainlint/multi-line-string.test
+++ b/t/chainlint/multi-line-string.test
@@ -3,25 +3,13 @@
line 2
line 3" &&
# LINT: missing "&&" on assignment
- y='line 1
- line2'
+ y="line 1
+ line2"
foobar
) &&
(
-# LINT: apostrophe (in a contraction) within string not misinterpreted as
-# LINT: starting multi-line single-quoted string
- echo "there's nothing to see here" &&
- exit
-) &&
-(
echo "xyz" "abc
def
ghi" &&
- echo 'xyz' 'abc
- def
- ghi' &&
- echo 'xyz' "abc
- def
- ghi" &&
barfoo
)
diff --git a/t/chainlint/negated-one-liner.expect b/t/chainlint/negated-one-liner.expect
index cf18429d03..ad4c2d949e 100644
--- a/t/chainlint/negated-one-liner.expect
+++ b/t/chainlint/negated-one-liner.expect
@@ -1,5 +1,5 @@
! (foo && bar) &&
! (foo && bar) >baz &&
-?!SEMI?!! (foo; bar) &&
-?!SEMI?!! (foo; bar) >baz
+! (foo; ?!AMP?! bar) &&
+! (foo; ?!AMP?! bar) >baz
diff --git a/t/chainlint/nested-cuddled-subshell.expect b/t/chainlint/nested-cuddled-subshell.expect
index c2a59ffc33..2a86885ee6 100644
--- a/t/chainlint/nested-cuddled-subshell.expect
+++ b/t/chainlint/nested-cuddled-subshell.expect
@@ -1,19 +1,19 @@
(
(cd foo &&
bar
->> ) &&
+ ) &&
(cd foo &&
bar
-?!AMP?!>> )
+ ) ?!AMP?!
(
cd foo &&
->> bar) &&
+ bar) &&
(
cd foo &&
-?!AMP?!>> bar)
+ bar) ?!AMP?!
(cd foo &&
->> bar) &&
+ bar) &&
(cd foo &&
-?!AMP?!>> bar)
+ bar) ?!AMP?!
foobar
->)
+)
diff --git a/t/chainlint/nested-here-doc.expect b/t/chainlint/nested-here-doc.expect
index 0c9ef1cfc6..e3bef63f75 100644
--- a/t/chainlint/nested-here-doc.expect
+++ b/t/chainlint/nested-here-doc.expect
@@ -1,7 +1,7 @@
-cat >foop &&
+cat <<ARBITRARY >foop &&
(
- cat &&
-?!AMP?! cat
+ cat <<-INPUT_END &&
+ cat <<-EOT ?!AMP?!
foobar
->)
+)
diff --git a/t/chainlint/nested-subshell-comment.expect b/t/chainlint/nested-subshell-comment.expect
index 15b68d4373..be4b27a305 100644
--- a/t/chainlint/nested-subshell-comment.expect
+++ b/t/chainlint/nested-subshell-comment.expect
@@ -2,10 +2,8 @@
foo &&
(
bar &&
- # bottles wobble while fiddles gobble
- # minor numbers of cows (or do they?)
baz &&
snaff
-?!AMP?!>> )
+ ) ?!AMP?!
fuzzy
->)
+)
diff --git a/t/chainlint/nested-subshell-comment.test b/t/chainlint/nested-subshell-comment.test
index 0ff136ab3c..0215cdb192 100644
--- a/t/chainlint/nested-subshell-comment.test
+++ b/t/chainlint/nested-subshell-comment.test
@@ -7,7 +7,7 @@
# minor numbers of cows (or do they?)
baz &&
snaff
-# LINT: missing "&&" on ')'
+# LINT: missing "&&" on ")"
)
fuzzy
)
diff --git a/t/chainlint/nested-subshell.expect b/t/chainlint/nested-subshell.expect
index c8165ad19e..41a48adaa2 100644
--- a/t/chainlint/nested-subshell.expect
+++ b/t/chainlint/nested-subshell.expect
@@ -3,10 +3,10 @@
(
echo a &&
echo b
->> ) >file &&
+ ) >file &&
cd foo &&
(
echo a
echo b
->> ) >file
->)
+ ) >file
+)
diff --git a/t/chainlint/nested-subshell.test b/t/chainlint/nested-subshell.test
index 998b05a47d..440ee9992d 100644
--- a/t/chainlint/nested-subshell.test
+++ b/t/chainlint/nested-subshell.test
@@ -7,7 +7,6 @@
cd foo &&
(
-# LINT: nested multi-line subshell not presently checked for missing "&&"
echo a
echo b
) >file
diff --git a/t/chainlint/not-heredoc.expect b/t/chainlint/not-heredoc.expect
new file mode 100644
index 0000000000..2e9bb135fe
--- /dev/null
+++ b/t/chainlint/not-heredoc.expect
@@ -0,0 +1,14 @@
+echo "<<<<<<< ours" &&
+echo ourside &&
+echo "=======" &&
+echo theirside &&
+echo ">>>>>>> theirs" &&
+
+(
+ echo "<<<<<<< ours" &&
+ echo ourside &&
+ echo "=======" &&
+ echo theirside &&
+ echo ">>>>>>> theirs" ?!AMP?!
+ poodle
+) >merged
diff --git a/t/chainlint/not-heredoc.test b/t/chainlint/not-heredoc.test
new file mode 100644
index 0000000000..9aa57346cd
--- /dev/null
+++ b/t/chainlint/not-heredoc.test
@@ -0,0 +1,16 @@
+# LINT: "<< ours" inside string is not here-doc
+echo "<<<<<<< ours" &&
+echo ourside &&
+echo "=======" &&
+echo theirside &&
+echo ">>>>>>> theirs" &&
+
+(
+# LINT: "<< ours" inside string is not here-doc
+ echo "<<<<<<< ours" &&
+ echo ourside &&
+ echo "=======" &&
+ echo theirside &&
+ echo ">>>>>>> theirs"
+ poodle
+) >merged
diff --git a/t/chainlint/one-liner.expect b/t/chainlint/one-liner.expect
index 237f227349..57a7a444c1 100644
--- a/t/chainlint/one-liner.expect
+++ b/t/chainlint/one-liner.expect
@@ -2,8 +2,8 @@
(foo && bar) |
(foo && bar) >baz &&
-?!SEMI?!(foo; bar) &&
-?!SEMI?!(foo; bar) |
-?!SEMI?!(foo; bar) >baz
+(foo; ?!AMP?! bar) &&
+(foo; ?!AMP?! bar) |
+(foo; ?!AMP?! bar) >baz &&
(foo "bar; baz")
diff --git a/t/chainlint/one-liner.test b/t/chainlint/one-liner.test
index ec9acb9825..be9858fa29 100644
--- a/t/chainlint/one-liner.test
+++ b/t/chainlint/one-liner.test
@@ -3,10 +3,10 @@
(foo && bar) |
(foo && bar) >baz &&
-# LINT: top-level one-liner subshell missing internal "&&"
+# LINT: top-level one-liner subshell missing internal "&&" and broken &&-chain
(foo; bar) &&
(foo; bar) |
-(foo; bar) >baz
+(foo; bar) >baz &&
# LINT: ";" in string not misinterpreted as broken &&-chain
(foo "bar; baz")
diff --git a/t/chainlint/p4-filespec.expect b/t/chainlint/p4-filespec.expect
index 98b3d881fd..1290fd1ff2 100644
--- a/t/chainlint/p4-filespec.expect
+++ b/t/chainlint/p4-filespec.expect
@@ -1,4 +1,4 @@
(
p4 print -1 //depot/fiddle#42 >file &&
foobar
->)
+)
diff --git a/t/chainlint/pipe.expect b/t/chainlint/pipe.expect
index 211b901dbc..2cfc028297 100644
--- a/t/chainlint/pipe.expect
+++ b/t/chainlint/pipe.expect
@@ -3,6 +3,6 @@
bar |
baz &&
fish |
-?!AMP?! cow
+ cow ?!AMP?!
sunder
->)
+)
diff --git a/t/chainlint/pipe.test b/t/chainlint/pipe.test
index e6af4de916..dd82534c66 100644
--- a/t/chainlint/pipe.test
+++ b/t/chainlint/pipe.test
@@ -4,7 +4,7 @@
bar |
baz &&
-# LINT: final line of pipe sequence ('cow') lacking "&&"
+# LINT: final line of pipe sequence ("cow") lacking "&&"
fish |
cow
diff --git a/t/chainlint/semicolon.expect b/t/chainlint/semicolon.expect
index 1d79384606..ed0b3707ae 100644
--- a/t/chainlint/semicolon.expect
+++ b/t/chainlint/semicolon.expect
@@ -1,20 +1,19 @@
(
-?!AMP?!?!SEMI?! cat foo ; echo bar
-?!SEMI?! cat foo ; echo bar
->) &&
+ cat foo ; ?!AMP?! echo bar ?!AMP?!
+ cat foo ; ?!AMP?! echo bar
+) &&
(
-?!SEMI?! cat foo ; echo bar &&
-?!SEMI?! cat foo ; echo bar
->) &&
+ cat foo ; ?!AMP?! echo bar &&
+ cat foo ; ?!AMP?! echo bar
+) &&
(
echo "foo; bar" &&
-?!SEMI?! cat foo; echo bar
->) &&
+ cat foo; ?!AMP?! echo bar
+) &&
(
-?!SEMI?! foo;
->) &&
-(
-cd foo &&
+ foo;
+) &&
+(cd foo &&
for i in a b c; do
-?!SEMI?! echo;
-> done)
+ echo;
+ done)
diff --git a/t/chainlint/semicolon.test b/t/chainlint/semicolon.test
index d82c8ebbc0..67e1192c50 100644
--- a/t/chainlint/semicolon.test
+++ b/t/chainlint/semicolon.test
@@ -15,11 +15,11 @@
cat foo; echo bar
) &&
(
-# LINT: unnecessary terminating semicolon
+# LINT: semicolon unnecessary but legitimate
foo;
) &&
(cd foo &&
for i in a b c; do
-# LINT: unnecessary terminating semicolon
+# LINT: semicolon unnecessary but legitimate
echo;
done)
diff --git a/t/chainlint/subshell-here-doc.expect b/t/chainlint/subshell-here-doc.expect
index 74723e7340..029d129299 100644
--- a/t/chainlint/subshell-here-doc.expect
+++ b/t/chainlint/subshell-here-doc.expect
@@ -1,11 +1,10 @@
(
- echo wobba gorgo snoot wafta snurb &&
-?!AMP?! cat >bip
- echo >bop
->) &&
+ echo wobba gorgo snoot wafta snurb <<-EOF &&
+ cat <<EOF >bip ?!AMP?!
+ echo <<-EOF >bop
+) &&
(
- cat >bup &&
- cat >bup2 &&
- cat >bup3 &&
+ cat <<-ARBITRARY >bup &&
+ cat <<-ARBITRARY3 >bup3 &&
meep
->)
+)
diff --git a/t/chainlint/subshell-here-doc.test b/t/chainlint/subshell-here-doc.test
index f6b3ba4214..d40eb65583 100644
--- a/t/chainlint/subshell-here-doc.test
+++ b/t/chainlint/subshell-here-doc.test
@@ -8,10 +8,10 @@
nevermore...
EOF
-# LINT: missing "&&" on 'cat'
+# LINT: missing "&&" on "cat"
cat <<EOF >bip
fish fly high
- EOF
+EOF
# LINT: swallow here-doc (EOF is last line of subshell)
echo <<-\EOF >bop
@@ -27,10 +27,6 @@
glink
FIZZ
ARBITRARY
- cat <<-'ARBITRARY2' >bup2 &&
- glink
- FIZZ
- ARBITRARY2
cat <<-"ARBITRARY3" >bup3 &&
glink
FIZZ
diff --git a/t/chainlint/subshell-one-liner.expect b/t/chainlint/subshell-one-liner.expect
index 51162821d7..b7015361bf 100644
--- a/t/chainlint/subshell-one-liner.expect
+++ b/t/chainlint/subshell-one-liner.expect
@@ -2,13 +2,13 @@
(foo && bar) &&
(foo && bar) |
(foo && bar) >baz &&
-?!SEMI?! (foo; bar) &&
-?!SEMI?! (foo; bar) |
-?!SEMI?! (foo; bar) >baz &&
+ (foo; ?!AMP?! bar) &&
+ (foo; ?!AMP?! bar) |
+ (foo; ?!AMP?! bar) >baz &&
(foo || exit 1) &&
(foo || exit 1) |
(foo || exit 1) >baz &&
-?!AMP?! (foo && bar)
-?!AMP?!?!SEMI?! (foo && bar; baz)
+ (foo && bar) ?!AMP?!
+ (foo && bar; ?!AMP?! baz) ?!AMP?!
foobar
->)
+)
diff --git a/t/chainlint/t7900-subtree.expect b/t/chainlint/t7900-subtree.expect
index c9913429e6..1cccc7bf7e 100644
--- a/t/chainlint/t7900-subtree.expect
+++ b/t/chainlint/t7900-subtree.expect
@@ -1,10 +1,10 @@
(
chks="sub1sub2sub3sub4" &&
- chks_sub=$(cat | sed 's,^,sub dir/,'
->>) &&
+ chks_sub=$(cat <<TXT | sed "s,^,sub dir/,"
+) &&
chkms="main-sub1main-sub2main-sub3main-sub4" &&
- chkms_sub=$(cat | sed 's,^,sub dir/,'
->>) &&
+ chkms_sub=$(cat <<TXT | sed "s,^,sub dir/,"
+) &&
subfiles=$(git ls-files) &&
check_equal "$subfiles" "$chkms$chks"
->)
+)
diff --git a/t/chainlint/t7900-subtree.test b/t/chainlint/t7900-subtree.test
index 277d8358df..02f3129232 100644
--- a/t/chainlint/t7900-subtree.test
+++ b/t/chainlint/t7900-subtree.test
@@ -3,7 +3,7 @@
sub2
sub3
sub4" &&
- chks_sub=$(cat <<TXT | sed 's,^,sub dir/,'
+ chks_sub=$(cat <<TXT | sed "s,^,sub dir/,"
$chks
TXT
) &&
@@ -11,7 +11,7 @@ TXT
main-sub2
main-sub3
main-sub4" &&
- chkms_sub=$(cat <<TXT | sed 's,^,sub dir/,'
+ chkms_sub=$(cat <<TXT | sed "s,^,sub dir/,"
$chkms
TXT
) &&
diff --git a/t/chainlint/while-loop.expect b/t/chainlint/while-loop.expect
index 13cff2c0a5..0d3a9b3d12 100644
--- a/t/chainlint/while-loop.expect
+++ b/t/chainlint/while-loop.expect
@@ -1,11 +1,11 @@
(
while true
do
-?!AMP?! echo foo
- cat
-?!AMP?! done
+ echo foo ?!AMP?!
+ cat <<-EOF
+ done ?!AMP?!
while true; do
echo foo &&
cat bar
done
->)
+)
diff --git a/t/chainlint/while-loop.test b/t/chainlint/while-loop.test
index f1df085bf0..d09fb016e4 100644
--- a/t/chainlint/while-loop.test
+++ b/t/chainlint/while-loop.test
@@ -1,17 +1,17 @@
(
-# LINT: 'while, 'do', 'done' do not need "&&"
+# LINT: "while", "do", "done" do not need "&&"
while true
do
-# LINT: missing "&&" on 'echo'
+# LINT: missing "&&" on "echo"
echo foo
# LINT: last statement of while does not need "&&"
cat <<-\EOF
bar
EOF
-# LINT: missing "&&" on 'done'
+# LINT: missing "&&" on "done"
done
-# LINT: 'do' on same line as 'while'
+# LINT: "do" on same line as "while"
while true; do
echo foo &&
cat bar
diff --git a/t/helper/test-trace2.c b/t/helper/test-trace2.c
index f93633f895..59b124bb5f 100644
--- a/t/helper/test-trace2.c
+++ b/t/helper/test-trace2.c
@@ -262,8 +262,9 @@ static int print_usage(void)
* [] the "cmd_name" event has been generated.
* [] this writes various "def_param" events for interesting config values.
*
- * We further assume that if we return (rather than exit()), trace2_cmd_exit()
- * will be called by test-tool.c:cmd_main().
+ * We return from here and let test-tool.c::cmd_main() pass the exit
+ * code to common-main.c::main(), which will use it to call
+ * trace2_cmd_exit().
*/
int cmd__trace2(int argc, const char **argv)
{
diff --git a/t/lib-gpg.sh b/t/lib-gpg.sh
index a3f285f515..3e7ee1386a 100644
--- a/t/lib-gpg.sh
+++ b/t/lib-gpg.sh
@@ -90,7 +90,12 @@ test_lazy_prereq RFC1991 '
GPGSSH_KEY_PRIMARY="${GNUPGHOME}/ed25519_ssh_signing_key"
GPGSSH_KEY_SECONDARY="${GNUPGHOME}/rsa_2048_ssh_signing_key"
GPGSSH_KEY_UNTRUSTED="${GNUPGHOME}/untrusted_ssh_signing_key"
+GPGSSH_KEY_EXPIRED="${GNUPGHOME}/expired_ssh_signing_key"
+GPGSSH_KEY_NOTYETVALID="${GNUPGHOME}/notyetvalid_ssh_signing_key"
+GPGSSH_KEY_TIMEBOXEDVALID="${GNUPGHOME}/timeboxed_valid_ssh_signing_key"
+GPGSSH_KEY_TIMEBOXEDINVALID="${GNUPGHOME}/timeboxed_invalid_ssh_signing_key"
GPGSSH_KEY_WITH_PASSPHRASE="${GNUPGHOME}/protected_ssh_signing_key"
+GPGSSH_KEY_ECDSA="${GNUPGHOME}/ecdsa_ssh_signing_key"
GPGSSH_KEY_PASSPHRASE="super_secret"
GPGSSH_ALLOWED_SIGNERS="${GNUPGHOME}/ssh.all_valid.allowedSignersFile"
@@ -105,21 +110,63 @@ test_lazy_prereq GPGSSH '
echo $ssh_version | grep -q "find-principals:missing signature file"
test $? = 0 || exit 1;
- # some broken versions of ssh-keygen segfault on find-principals;
- # avoid testing with them.
- ssh-keygen -Y find-principals -f /dev/null -s /dev/null
- test $? = 139 && exit 1
-
+ # Setup some keys and an allowed signers file
mkdir -p "${GNUPGHOME}" &&
chmod 0700 "${GNUPGHOME}" &&
(setfacl -k "${GNUPGHOME}" 2>/dev/null || true) &&
ssh-keygen -t ed25519 -N "" -C "git ed25519 key" -f "${GPGSSH_KEY_PRIMARY}" >/dev/null &&
- echo "\"principal with number 1\" $(cat "${GPGSSH_KEY_PRIMARY}.pub")" >> "${GPGSSH_ALLOWED_SIGNERS}" &&
ssh-keygen -t rsa -b 2048 -N "" -C "git rsa2048 key" -f "${GPGSSH_KEY_SECONDARY}" >/dev/null &&
- echo "\"principal with number 2\" $(cat "${GPGSSH_KEY_SECONDARY}.pub")" >> "${GPGSSH_ALLOWED_SIGNERS}" &&
ssh-keygen -t ed25519 -N "${GPGSSH_KEY_PASSPHRASE}" -C "git ed25519 encrypted key" -f "${GPGSSH_KEY_WITH_PASSPHRASE}" >/dev/null &&
- echo "\"principal with number 3\" $(cat "${GPGSSH_KEY_WITH_PASSPHRASE}.pub")" >> "${GPGSSH_ALLOWED_SIGNERS}" &&
- ssh-keygen -t ed25519 -N "" -f "${GPGSSH_KEY_UNTRUSTED}" >/dev/null
+ ssh-keygen -t ecdsa -N "" -f "${GPGSSH_KEY_ECDSA}" >/dev/null &&
+ ssh-keygen -t ed25519 -N "" -C "git ed25519 key" -f "${GPGSSH_KEY_UNTRUSTED}" >/dev/null &&
+
+ cat >"${GPGSSH_ALLOWED_SIGNERS}" <<-EOF &&
+ "principal with number 1" $(cat "${GPGSSH_KEY_PRIMARY}.pub")"
+ "principal with number 2" $(cat "${GPGSSH_KEY_SECONDARY}.pub")"
+ "principal with number 3" $(cat "${GPGSSH_KEY_WITH_PASSPHRASE}.pub")"
+ "principal with number 4" $(cat "${GPGSSH_KEY_ECDSA}.pub")"
+ EOF
+
+ # Verify if at least one key and ssh-keygen works as expected
+ echo "testpayload" |
+ ssh-keygen -Y sign -n "git" -f "${GPGSSH_KEY_PRIMARY}" >gpgssh_prereq.sig &&
+ ssh-keygen -Y find-principals -f "${GPGSSH_ALLOWED_SIGNERS}" -s gpgssh_prereq.sig &&
+ echo "testpayload" |
+ ssh-keygen -Y verify -n "git" -f "${GPGSSH_ALLOWED_SIGNERS}" -I "principal with number 1" -s gpgssh_prereq.sig
+'
+
+test_lazy_prereq GPGSSH_VERIFYTIME '
+ # Check if ssh-keygen has a verify-time option by passing an invalid date to it
+ ssh-keygen -Overify-time=INVALID -Y check-novalidate -s doesnotmatter 2>&1 | grep -q -F "Invalid \"verify-time\"" &&
+
+ # Set up keys with key lifetimes
+ ssh-keygen -t ed25519 -N "" -C "timeboxed valid key" -f "${GPGSSH_KEY_TIMEBOXEDVALID}" >/dev/null &&
+ key_valid=$(cat "${GPGSSH_KEY_TIMEBOXEDVALID}.pub") &&
+ ssh-keygen -t ed25519 -N "" -C "timeboxed invalid key" -f "${GPGSSH_KEY_TIMEBOXEDINVALID}" >/dev/null &&
+ key_invalid=$(cat "${GPGSSH_KEY_TIMEBOXEDINVALID}.pub") &&
+ ssh-keygen -t ed25519 -N "" -C "expired key" -f "${GPGSSH_KEY_EXPIRED}" >/dev/null &&
+ key_expired=$(cat "${GPGSSH_KEY_EXPIRED}.pub") &&
+ ssh-keygen -t ed25519 -N "" -C "not yet valid key" -f "${GPGSSH_KEY_NOTYETVALID}" >/dev/null &&
+ key_notyetvalid=$(cat "${GPGSSH_KEY_NOTYETVALID}.pub") &&
+
+ # Timestamps outside of test_tick span
+ ts2005a=20050401000000 ts2005b=200504020000 &&
+ # Timestamps within test_tick span
+ ts2005c=20050407000000 ts2005d=200504100000 &&
+ # Definitely not yet valid / expired timestamps
+ ts2000=20000101000000 ts2999=29990101000000 &&
+
+ cat >>"${GPGSSH_ALLOWED_SIGNERS}" <<-EOF &&
+ "timeboxed valid key" valid-after="$ts2005c",valid-before="$ts2005d" $key_valid"
+ "timeboxed invalid key" valid-after="$ts2005a",valid-before="$ts2005b" $key_invalid"
+ "principal with expired key" valid-before="$ts2000" $key_expired"
+ "principal with not yet valid key" valid-after="$ts2999" $key_notyetvalid"
+ EOF
+
+ # and verify ssh-keygen verifies the key lifetime
+ echo "testpayload" |
+ ssh-keygen -Y sign -n "git" -f "${GPGSSH_KEY_EXPIRED}" >gpgssh_verifytime_prereq.sig &&
+ ! (ssh-keygen -Y verify -n "git" -f "${GPGSSH_ALLOWED_SIGNERS}" -I "principal with expired key" -s gpgssh_verifytime_prereq.sig)
'
sanitize_pgp() {
diff --git a/t/perf/p2000-sparse-operations.sh b/t/perf/p2000-sparse-operations.sh
index bfd332120c..cb777c74a2 100755
--- a/t/perf/p2000-sparse-operations.sh
+++ b/t/perf/p2000-sparse-operations.sh
@@ -113,5 +113,9 @@ test_perf_on_all git checkout -f -
test_perf_on_all git reset
test_perf_on_all git reset --hard
test_perf_on_all git reset -- does-not-exist
+test_perf_on_all git diff
+test_perf_on_all git diff --cached
+test_perf_on_all git blame $SPARSE_CONE/a
+test_perf_on_all git blame $SPARSE_CONE/f3/a
test_done
diff --git a/t/t1092-sparse-checkout-compatibility.sh b/t/t1092-sparse-checkout-compatibility.sh
index f8270943aa..49f70a6569 100755
--- a/t/t1092-sparse-checkout-compatibility.sh
+++ b/t/t1092-sparse-checkout-compatibility.sh
@@ -415,7 +415,7 @@ test_expect_success 'checkout and reset --hard' '
test_all_match git reset --hard update-folder2
'
-test_expect_success 'diff --staged' '
+test_expect_success 'diff --cached' '
init_repos &&
write_script edit-contents <<-\EOF &&
@@ -424,10 +424,10 @@ test_expect_success 'diff --staged' '
run_on_all ../edit-contents &&
test_all_match git diff &&
- test_all_match git diff --staged &&
+ test_all_match git diff --cached &&
test_all_match git add README.md &&
test_all_match git diff &&
- test_all_match git diff --staged
+ test_all_match git diff --cached
'
# NEEDSWORK: sparse-checkout behaves differently from full-checkout when
@@ -444,8 +444,8 @@ test_expect_success 'diff with renames and conflicts' '
test_all_match git checkout rename-base &&
test_all_match git checkout $branch -- . &&
test_all_match git status --porcelain=v2 &&
- test_all_match git diff --staged --no-renames &&
- test_all_match git diff --staged --find-renames || return 1
+ test_all_match git diff --cached --no-renames &&
+ test_all_match git diff --cached --find-renames || return 1
done
'
@@ -464,8 +464,8 @@ test_expect_success 'diff with directory/file conflicts' '
test_all_match git checkout $branch &&
test_all_match git checkout rename-base -- . &&
test_all_match git status --porcelain=v2 &&
- test_all_match git diff --staged --no-renames &&
- test_all_match git diff --staged --find-renames || return 1
+ test_all_match git diff --cached --no-renames &&
+ test_all_match git diff --cached --find-renames || return 1
done
'
@@ -486,21 +486,36 @@ test_expect_success 'log with pathspec outside sparse definition' '
test_expect_success 'blame with pathspec inside sparse definition' '
init_repos &&
- test_all_match git blame a &&
- test_all_match git blame deep/a &&
- test_all_match git blame deep/deeper1/a &&
- test_all_match git blame deep/deeper1/deepest/a
+ for file in a \
+ deep/a \
+ deep/deeper1/a \
+ deep/deeper1/deepest/a
+ do
+ test_all_match git blame $file
+ done
'
-# TODO: blame currently does not support blaming files outside of the
-# sparse definition. It complains that the file doesn't exist locally.
-test_expect_failure 'blame with pathspec outside sparse definition' '
+# Without a revision specified, blame will error if passed any file that
+# is not present in the working directory (even if the file is tracked).
+# Here we just verify that this is also true with sparse checkouts.
+test_expect_success 'blame with pathspec outside sparse definition' '
init_repos &&
+ test_sparse_match git sparse-checkout set &&
- test_all_match git blame folder1/a &&
- test_all_match git blame folder2/a &&
- test_all_match git blame deep/deeper2/a &&
- test_all_match git blame deep/deeper2/deepest/a
+ for file in a \
+ deep/a \
+ deep/deeper1/a \
+ deep/deeper1/deepest/a
+ do
+ test_sparse_match test_must_fail git blame $file &&
+ cat >expect <<-EOF &&
+ fatal: Cannot lstat '"'"'$file'"'"': No such file or directory
+ EOF
+ # We compare sparse-checkout-err and sparse-index-err in
+ # `test_sparse_match`. Given we know they are the same, we
+ # only check the content of sparse-index-err here.
+ test_cmp expect sparse-index-err
+ done
'
test_expect_success 'checkout and reset (mixed)' '
@@ -936,6 +951,64 @@ test_expect_success 'sparse-index is not expanded: merge conflict in cone' '
)
'
+test_expect_success 'sparse index is not expanded: diff' '
+ init_repos &&
+
+ write_script edit-contents <<-\EOF &&
+ echo text >>$1
+ EOF
+
+ # Add file within cone
+ test_sparse_match git sparse-checkout set deep &&
+ run_on_all ../edit-contents deep/testfile &&
+ test_all_match git add deep/testfile &&
+ run_on_all ../edit-contents deep/testfile &&
+
+ test_all_match git diff &&
+ test_all_match git diff --cached &&
+ ensure_not_expanded diff &&
+ ensure_not_expanded diff --cached &&
+
+ # Add file outside cone
+ test_all_match git reset --hard &&
+ run_on_all mkdir newdirectory &&
+ run_on_all ../edit-contents newdirectory/testfile &&
+ test_sparse_match git sparse-checkout set newdirectory &&
+ test_all_match git add newdirectory/testfile &&
+ run_on_all ../edit-contents newdirectory/testfile &&
+ test_sparse_match git sparse-checkout set &&
+
+ test_all_match git diff &&
+ test_all_match git diff --cached &&
+ ensure_not_expanded diff &&
+ ensure_not_expanded diff --cached &&
+
+ # Merge conflict outside cone
+ # The sparse checkout will report a warning that is not in the
+ # full checkout, so we use `run_on_all` instead of
+ # `test_all_match`
+ run_on_all git reset --hard &&
+ test_all_match git checkout merge-left &&
+ test_all_match test_must_fail git merge merge-right &&
+
+ test_all_match git diff &&
+ test_all_match git diff --cached &&
+ ensure_not_expanded diff &&
+ ensure_not_expanded diff --cached
+'
+
+test_expect_success 'sparse index is not expanded: blame' '
+ init_repos &&
+
+ for file in a \
+ deep/a \
+ deep/deeper1/a \
+ deep/deeper1/deepest/a
+ do
+ ensure_not_expanded blame $file
+ done
+'
+
# NEEDSWORK: a sparse-checkout behaves differently from a full checkout
# in this scenario, but it shouldn't.
test_expect_success 'reset mixed and checkout orphan' '
diff --git a/t/t2018-checkout-branch.sh b/t/t2018-checkout-branch.sh
index 93be1c0eae..3e93506c04 100755
--- a/t/t2018-checkout-branch.sh
+++ b/t/t2018-checkout-branch.sh
@@ -148,7 +148,7 @@ test_expect_success 'checkout -b to an existing branch fails' '
test_expect_success 'checkout -b to @{-1} fails with the right branch name' '
git checkout branch1 &&
git checkout branch2 &&
- echo >expect "fatal: A branch named '\''branch1'\'' already exists." &&
+ echo >expect "fatal: a branch named '\''branch1'\'' already exists" &&
test_must_fail git checkout -b @{-1} 2>actual &&
test_cmp expect actual
'
diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh
index 8c5c1ccf33..8a619d785e 100755
--- a/t/t3200-branch.sh
+++ b/t/t3200-branch.sh
@@ -168,6 +168,13 @@ test_expect_success 'git branch -M foo bar should fail when bar is checked out'
test_must_fail git branch -M bar foo
'
+test_expect_success 'git branch -M foo bar should fail when bar is checked out in worktree' '
+ git branch -f bar &&
+ test_when_finished "git worktree remove wt && git branch -D wt" &&
+ git worktree add wt &&
+ test_must_fail git branch -M bar wt
+'
+
test_expect_success 'git branch -M baz bam should succeed when baz is checked out' '
git checkout -b baz &&
git branch bam &&
@@ -888,7 +895,7 @@ test_expect_success '--set-upstream-to fails on a missing src branch' '
'
test_expect_success '--set-upstream-to fails on a non-ref' '
- echo "fatal: Cannot setup tracking information; starting point '"'"'HEAD^{}'"'"' is not a branch." >expect &&
+ echo "fatal: cannot set up tracking information; starting point '"'"'HEAD^{}'"'"' is not a branch" >expect &&
test_must_fail git branch --set-upstream-to HEAD^{} 2>err &&
test_cmp expect err
'
@@ -975,7 +982,7 @@ test_expect_success 'disabled option --set-upstream fails' '
test_expect_success '--set-upstream-to notices an error to set branch as own upstream' '
git branch --set-upstream-to refs/heads/my13 my13 2>actual &&
cat >expect <<-\EOF &&
- warning: Not setting branch my13 as its own upstream.
+ warning: not setting branch my13 as its own upstream
EOF
test_expect_code 1 git config branch.my13.remote &&
test_expect_code 1 git config branch.my13.merge &&
diff --git a/t/t3409-rebase-environ.sh b/t/t3409-rebase-environ.sh
new file mode 100755
index 0000000000..83ffb39d9f
--- /dev/null
+++ b/t/t3409-rebase-environ.sh
@@ -0,0 +1,23 @@
+#!/bin/sh
+
+test_description='git rebase interactive environment'
+
+. ./test-lib.sh
+
+test_expect_success 'setup' '
+ test_commit one &&
+ test_commit two &&
+ test_commit three
+'
+
+test_expect_success 'rebase --exec does not muck with GIT_DIR' '
+ git rebase --exec "printf %s \$GIT_DIR >environ" HEAD~1 &&
+ test_must_be_empty environ
+'
+
+test_expect_success 'rebase --exec does not muck with GIT_WORK_TREE' '
+ git rebase --exec "printf %s \$GIT_WORK_TREE >environ" HEAD~1 &&
+ test_must_be_empty environ
+'
+
+test_done
diff --git a/t/t4126-apply-empty.sh b/t/t4126-apply-empty.sh
index a361e79a81..82284d2f45 100755
--- a/t/t4126-apply-empty.sh
+++ b/t/t4126-apply-empty.sh
@@ -11,6 +11,8 @@ test_expect_success setup '
git add empty &&
test_tick &&
git commit -m initial &&
+ git commit --allow-empty -m "empty commit" &&
+ git format-patch --always HEAD~ >empty.patch &&
for i in a b c d e
do
echo $i
@@ -27,30 +29,42 @@ test_expect_success setup '
'
test_expect_success 'apply empty' '
- git reset --hard &&
rm -f missing &&
+ test_when_finished "git reset --hard" &&
git apply patch0 &&
test_cmp expect empty
'
+test_expect_success 'apply empty patch fails' '
+ test_when_finished "git reset --hard" &&
+ test_must_fail git apply empty.patch &&
+ test_must_fail git apply - </dev/null
+'
+
+test_expect_success 'apply with --allow-empty succeeds' '
+ test_when_finished "git reset --hard" &&
+ git apply --allow-empty empty.patch &&
+ git apply --allow-empty - </dev/null
+'
+
test_expect_success 'apply --index empty' '
- git reset --hard &&
rm -f missing &&
+ test_when_finished "git reset --hard" &&
git apply --index patch0 &&
test_cmp expect empty &&
git diff --exit-code
'
test_expect_success 'apply create' '
- git reset --hard &&
rm -f missing &&
+ test_when_finished "git reset --hard" &&
git apply patch1 &&
test_cmp expect missing
'
test_expect_success 'apply --index create' '
- git reset --hard &&
rm -f missing &&
+ test_when_finished "git reset --hard" &&
git apply --index patch1 &&
test_cmp expect missing &&
git diff --exit-code
diff --git a/t/t4202-log.sh b/t/t4202-log.sh
index 7884e3d46b..2ced7e9d81 100755
--- a/t/t4202-log.sh
+++ b/t/t4202-log.sh
@@ -952,6 +952,43 @@ test_expect_success 'decorate-refs-exclude and simplify-by-decoration' '
test_cmp expect.decorate actual
'
+test_expect_success 'decorate-refs with implied decorate from format' '
+ cat >expect <<-\EOF &&
+ side-2 (tag: side-2)
+ side-1
+ EOF
+ git log --no-walk --format="%s%d" \
+ --decorate-refs="*side-2" side-1 side-2 \
+ >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'implied decorate does not override option' '
+ cat >expect <<-\EOF &&
+ side-2 (tag: refs/tags/side-2, refs/heads/side)
+ side-1 (tag: refs/tags/side-1)
+ EOF
+ git log --no-walk --format="%s%d" \
+ --decorate=full side-1 side-2 \
+ >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'decorate-refs and simplify-by-decoration without output' '
+ cat >expect <<-\EOF &&
+ side-2
+ initial
+ EOF
+ # Do not just use a --format without %d here; we want to
+ # make sure that we did not accidentally turn on displaying
+ # the decorations, too. And that requires one of the regular
+ # formats.
+ git log --decorate-refs="*side-2" --oneline \
+ --simplify-by-decoration >actual.raw &&
+ sed "s/^[0-9a-f]* //" <actual.raw >actual &&
+ test_cmp expect actual
+'
+
test_expect_success 'log.decorate config parsing' '
git log --oneline --decorate=full >expect.full &&
git log --oneline --decorate=short >expect.short &&
@@ -1677,6 +1714,24 @@ test_expect_success GPGSSH 'setup sshkey signed branch' '
git commit -S -m signed_commit
'
+test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'create signed commits with keys having defined lifetimes' '
+ test_config gpg.format ssh &&
+ touch file &&
+ git add file &&
+
+ echo expired >file && test_tick && git commit -a -m expired -S"${GPGSSH_KEY_EXPIRED}" &&
+ git tag expired-signed &&
+
+ echo notyetvalid >file && test_tick && git commit -a -m notyetvalid -S"${GPGSSH_KEY_NOTYETVALID}" &&
+ git tag notyetvalid-signed &&
+
+ echo timeboxedvalid >file && test_tick && git commit -a -m timeboxedvalid -S"${GPGSSH_KEY_TIMEBOXEDVALID}" &&
+ git tag timeboxedvalid-signed &&
+
+ echo timeboxedinvalid >file && test_tick && git commit -a -m timeboxedinvalid -S"${GPGSSH_KEY_TIMEBOXEDINVALID}" &&
+ git tag timeboxedinvalid-signed
+'
+
test_expect_success GPGSM 'log x509 fingerprint' '
echo "F8BF62E0693D0694816377099909C779FA23FD65 | " >expect &&
git log -n1 --format="%GF | %GP" signed-x509 >actual &&
@@ -1714,6 +1769,31 @@ test_expect_success GPGSSH 'log --graph --show-signature ssh' '
grep "${GOOD_SIGNATURE_TRUSTED}" actual
'
+test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'log shows failure on expired signature key' '
+ test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
+ git log --graph --show-signature -n1 expired-signed >actual &&
+ ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
+'
+
+test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'log shows failure on not yet valid signature key' '
+ test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
+ git log --graph --show-signature -n1 notyetvalid-signed >actual &&
+ ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
+'
+
+test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'log show success with commit date and key validity matching' '
+ test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
+ git log --graph --show-signature -n1 timeboxedvalid-signed >actual &&
+ grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
+ ! grep "${GPGSSH_BAD_SIGNATURE}" actual
+'
+
+test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'log shows failure with commit date outside of key validity' '
+ test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
+ git log --graph --show-signature -n1 timeboxedinvalid-signed >actual &&
+ ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
+'
+
test_expect_success GPG 'log --graph --show-signature for merged tag' '
test_when_finished "git reset --hard && git checkout main" &&
git checkout -b plain main &&
diff --git a/t/t5504-fetch-receive-strict.sh b/t/t5504-fetch-receive-strict.sh
index 6e5a9c20e7..b0b795aca9 100755
--- a/t/t5504-fetch-receive-strict.sh
+++ b/t/t5504-fetch-receive-strict.sh
@@ -292,7 +292,7 @@ test_expect_success 'push with receive.fsck.missingEmail=warn' '
receive.fsck.missingEmail warn &&
git push --porcelain dst bogus >act 2>&1 &&
grep "missingEmail" act &&
- test_i18ngrep "Skipping unknown msg id.*whatever" act &&
+ test_i18ngrep "skipping unknown msg id.*whatever" act &&
git --git-dir=dst/.git branch -D bogus &&
git --git-dir=dst/.git config --add \
receive.fsck.missingEmail ignore &&
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index 7831a38dde..837a49642a 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1778,6 +1778,38 @@ test_expect_success 'denyCurrentBranch and worktrees' '
test_must_fail git -C cloned push origin HEAD:new-wt &&
test_config receive.denyCurrentBranch updateInstead &&
git -C cloned push origin HEAD:new-wt &&
+ test_path_exists new-wt/first.t &&
test_must_fail git -C cloned push --delete origin new-wt
'
+
+test_expect_success 'denyCurrentBranch and bare repository worktrees' '
+ test_when_finished "rm -fr bare.git" &&
+ git clone --bare . bare.git &&
+ git -C bare.git worktree add wt &&
+ test_commit grape &&
+ git -C bare.git config receive.denyCurrentBranch refuse &&
+ test_must_fail git push bare.git HEAD:wt &&
+ git -C bare.git config receive.denyCurrentBranch updateInstead &&
+ git push bare.git HEAD:wt &&
+ test_path_exists bare.git/wt/grape.t &&
+ test_must_fail git push --delete bare.git wt
+'
+
+test_expect_success 'refuse fetch to current branch of worktree' '
+ test_when_finished "git worktree remove --force wt && git branch -D wt" &&
+ git worktree add wt &&
+ test_commit apple &&
+ test_must_fail git fetch . HEAD:wt &&
+ git fetch -u . HEAD:wt
+'
+
+test_expect_success 'refuse fetch to current branch of bare repository worktree' '
+ test_when_finished "rm -fr bare.git" &&
+ git clone --bare . bare.git &&
+ git -C bare.git worktree add wt &&
+ test_commit banana &&
+ test_must_fail git -C bare.git fetch .. HEAD:wt &&
+ git -C bare.git fetch -u .. HEAD:wt
+'
+
test_done
diff --git a/t/t5553-set-upstream.sh b/t/t5553-set-upstream.sh
index 9c12c0f8c3..48050162c2 100755
--- a/t/t5553-set-upstream.sh
+++ b/t/t5553-set-upstream.sh
@@ -91,6 +91,17 @@ test_expect_success 'fetch --set-upstream with valid URL sets upstream to URL' '
check_config_missing other2
'
+test_expect_success 'fetch --set-upstream with a detached HEAD' '
+ git checkout HEAD^0 &&
+ test_when_finished "git checkout -" &&
+ cat >expect <<-\EOF &&
+ warning: could not set upstream of HEAD to '"'"'main'"'"' from '"'"'upstream'"'"' when it does not point to any branch.
+ EOF
+ git fetch --set-upstream upstream main 2>actual.raw &&
+ grep ^warning: actual.raw >actual &&
+ test_cmp expect actual
+'
+
# tests for pull --set-upstream
test_expect_success 'setup bare parent pull' '
@@ -178,4 +189,15 @@ test_expect_success 'pull --set-upstream with valid URL and branch sets branch'
check_config_missing other2
'
+test_expect_success 'pull --set-upstream with a detached HEAD' '
+ git checkout HEAD^0 &&
+ test_when_finished "git checkout -" &&
+ cat >expect <<-\EOF &&
+ warning: could not set upstream of HEAD to '"'"'main'"'"' from '"'"'upstream'"'"' when it does not point to any branch.
+ EOF
+ git pull --no-rebase --set-upstream upstream main 2>actual.raw &&
+ grep ^warning: actual.raw >actual &&
+ test_cmp expect actual
+'
+
test_done
diff --git a/t/t6200-fmt-merge-msg.sh b/t/t6200-fmt-merge-msg.sh
index 06c5fb5615..6e10a539ce 100755
--- a/t/t6200-fmt-merge-msg.sh
+++ b/t/t6200-fmt-merge-msg.sh
@@ -91,6 +91,26 @@ test_expect_success GPGSSH 'created ssh signed commit and tag' '
git tag -s -u"${GPGSSH_KEY_UNTRUSTED}" -m signed-ssh-tag-msg-untrusted signed-untrusted-ssh-tag left
'
+test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'create signed tags with keys having defined lifetimes' '
+ test_when_finished "test_unconfig commit.gpgsign" &&
+ test_config gpg.format ssh &&
+ git checkout -b signed-expiry-ssh &&
+ touch file &&
+ git add file &&
+
+ echo expired >file && test_tick && git commit -a -m expired -S"${GPGSSH_KEY_EXPIRED}" &&
+ git tag -s -u "${GPGSSH_KEY_EXPIRED}" -m expired-signed expired-signed &&
+
+ echo notyetvalid >file && test_tick && git commit -a -m notyetvalid -S"${GPGSSH_KEY_NOTYETVALID}" &&
+ git tag -s -u "${GPGSSH_KEY_NOTYETVALID}" -m notyetvalid-signed notyetvalid-signed &&
+
+ echo timeboxedvalid >file && test_tick && git commit -a -m timeboxedvalid -S"${GPGSSH_KEY_TIMEBOXEDVALID}" &&
+ git tag -s -u "${GPGSSH_KEY_TIMEBOXEDVALID}" -m timeboxedvalid-signed timeboxedvalid-signed &&
+
+ echo timeboxedinvalid >file && test_tick && git commit -a -m timeboxedinvalid -S"${GPGSSH_KEY_TIMEBOXEDINVALID}" &&
+ git tag -s -u "${GPGSSH_KEY_TIMEBOXEDINVALID}" -m timeboxedinvalid-signed timeboxedinvalid-signed
+'
+
test_expect_success 'message for merging local branch' '
echo "Merge branch ${apos}left${apos}" >expected &&
@@ -104,7 +124,7 @@ test_expect_success 'message for merging local branch' '
test_expect_success GPG 'message for merging local tag signed by good key' '
git checkout main &&
git fetch . signed-good-tag &&
- git fmt-merge-msg <.git/FETCH_HEAD >actual 2>&1 &&
+ git fmt-merge-msg <.git/FETCH_HEAD >actual &&
grep "^Merge tag ${apos}signed-good-tag${apos}" actual &&
grep "^# gpg: Signature made" actual &&
grep "^# gpg: Good signature from" actual
@@ -113,7 +133,7 @@ test_expect_success GPG 'message for merging local tag signed by good key' '
test_expect_success GPG 'message for merging local tag signed by unknown key' '
git checkout main &&
git fetch . signed-good-tag &&
- GNUPGHOME=. git fmt-merge-msg <.git/FETCH_HEAD >actual 2>&1 &&
+ GNUPGHOME=. git fmt-merge-msg <.git/FETCH_HEAD >actual &&
grep "^Merge tag ${apos}signed-good-tag${apos}" actual &&
grep "^# gpg: Signature made" actual &&
grep -E "^# gpg: Can${apos}t check signature: (public key not found|No public key)" actual
@@ -123,7 +143,8 @@ test_expect_success GPGSSH 'message for merging local tag signed by good ssh key
test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
git checkout main &&
git fetch . signed-good-ssh-tag &&
- git fmt-merge-msg <.git/FETCH_HEAD >actual 2>&1 &&
+ git fmt-merge-msg <.git/FETCH_HEAD >actual &&
+ grep "^Merge tag ${apos}signed-good-ssh-tag${apos}" actual &&
grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
! grep "${GPGSSH_BAD_SIGNATURE}" actual
'
@@ -132,11 +153,50 @@ test_expect_success GPGSSH 'message for merging local tag signed by unknown ssh
test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
git checkout main &&
git fetch . signed-untrusted-ssh-tag &&
- git fmt-merge-msg <.git/FETCH_HEAD >actual 2>&1 &&
+ git fmt-merge-msg <.git/FETCH_HEAD >actual &&
+ grep "^Merge tag ${apos}signed-untrusted-ssh-tag${apos}" actual &&
grep "${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}" actual &&
! grep "${GPGSSH_BAD_SIGNATURE}" actual &&
grep "${GPGSSH_KEY_NOT_TRUSTED}" actual
'
+
+test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'message for merging local tag signed by expired ssh key' '
+ test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
+ git checkout main &&
+ git fetch . expired-signed &&
+ git fmt-merge-msg <.git/FETCH_HEAD >actual &&
+ grep "^Merge tag ${apos}expired-signed${apos}" actual &&
+ ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
+'
+
+test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'message for merging local tag signed by not yet valid ssh key' '
+ test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
+ git checkout main &&
+ git fetch . notyetvalid-signed &&
+ git fmt-merge-msg <.git/FETCH_HEAD >actual &&
+ grep "^Merge tag ${apos}notyetvalid-signed${apos}" actual &&
+ ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
+'
+
+test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'message for merging local tag signed by valid timeboxed ssh key' '
+ test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
+ git checkout main &&
+ git fetch . timeboxedvalid-signed &&
+ git fmt-merge-msg <.git/FETCH_HEAD >actual &&
+ grep "^Merge tag ${apos}timeboxedvalid-signed${apos}" actual &&
+ grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
+ ! grep "${GPGSSH_BAD_SIGNATURE}" actual
+'
+
+test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'message for merging local tag signed by invalid timeboxed ssh key' '
+ test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
+ git checkout main &&
+ git fetch . timeboxedinvalid-signed &&
+ git fmt-merge-msg <.git/FETCH_HEAD >actual &&
+ grep "^Merge tag ${apos}timeboxedinvalid-signed${apos}" actual &&
+ ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
+'
+
test_expect_success 'message for merging external branch' '
echo "Merge branch ${apos}left${apos} of $(pwd)" >expected &&
diff --git a/t/t7031-verify-tag-signed-ssh.sh b/t/t7031-verify-tag-signed-ssh.sh
index 06c9dd6c93..1cb36b9ab8 100755
--- a/t/t7031-verify-tag-signed-ssh.sh
+++ b/t/t7031-verify-tag-signed-ssh.sh
@@ -48,6 +48,23 @@ test_expect_success GPGSSH 'create signed tags ssh' '
git tag -u"${GPGSSH_KEY_UNTRUSTED}" -m eighth eighth-signed-alt
'
+test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'create signed tags with keys having defined lifetimes' '
+ test_when_finished "test_unconfig commit.gpgsign" &&
+ test_config gpg.format ssh &&
+
+ echo expired >file && test_tick && git commit -a -m expired -S"${GPGSSH_KEY_EXPIRED}" &&
+ git tag -s -u "${GPGSSH_KEY_EXPIRED}" -m expired-signed expired-signed &&
+
+ echo notyetvalid >file && test_tick && git commit -a -m notyetvalid -S"${GPGSSH_KEY_NOTYETVALID}" &&
+ git tag -s -u "${GPGSSH_KEY_NOTYETVALID}" -m notyetvalid-signed notyetvalid-signed &&
+
+ echo timeboxedvalid >file && test_tick && git commit -a -m timeboxedvalid -S"${GPGSSH_KEY_TIMEBOXEDVALID}" &&
+ git tag -s -u "${GPGSSH_KEY_TIMEBOXEDVALID}" -m timeboxedvalid-signed timeboxedvalid-signed &&
+
+ echo timeboxedinvalid >file && test_tick && git commit -a -m timeboxedinvalid -S"${GPGSSH_KEY_TIMEBOXEDINVALID}" &&
+ git tag -s -u "${GPGSSH_KEY_TIMEBOXEDINVALID}" -m timeboxedinvalid-signed timeboxedinvalid-signed
+'
+
test_expect_success GPGSSH 'verify and show ssh signatures' '
test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
(
@@ -80,6 +97,31 @@ test_expect_success GPGSSH 'verify and show ssh signatures' '
)
'
+test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'verify-tag exits failure on expired signature key' '
+ test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
+ test_must_fail git verify-tag expired-signed 2>actual &&
+ ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
+'
+
+test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'verify-tag exits failure on not yet valid signature key' '
+ test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
+ test_must_fail git verify-tag notyetvalid-signed 2>actual &&
+ ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
+'
+
+test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'verify-tag succeeds with tag date and key validity matching' '
+ test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
+ git verify-tag timeboxedvalid-signed 2>actual &&
+ grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
+ ! grep "${GPGSSH_BAD_SIGNATURE}" actual
+'
+
+test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'verify-tag failes with tag date outside of key validity' '
+ test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
+ test_must_fail git verify-tag timeboxedinvalid-signed 2>actual &&
+ ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
+'
+
test_expect_success GPGSSH 'detect fudged ssh signature' '
test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
git cat-file tag seventh-signed >raw &&
diff --git a/t/t7510-signed-commit.sh b/t/t7510-signed-commit.sh
index d65a0171f2..9882b69ae2 100755
--- a/t/t7510-signed-commit.sh
+++ b/t/t7510-signed-commit.sh
@@ -228,7 +228,7 @@ test_expect_success GPG 'detect fudged signature with NUL' '
'
test_expect_success GPG 'amending already signed commit' '
- git checkout fourth-signed^0 &&
+ git checkout -f fourth-signed^0 &&
git commit --amend -S --no-edit &&
git verify-commit HEAD &&
git show -s --show-signature HEAD >actual &&
diff --git a/t/t7528-signed-commit-ssh.sh b/t/t7528-signed-commit-ssh.sh
index badf3ed320..f47e995179 100755
--- a/t/t7528-signed-commit-ssh.sh
+++ b/t/t7528-signed-commit-ssh.sh
@@ -73,7 +73,46 @@ test_expect_success GPGSSH 'create signed commits' '
git tag eleventh-signed $(cat oid) &&
echo 12 | git commit-tree --gpg-sign="${GPGSSH_KEY_UNTRUSTED}" HEAD^{tree} >oid &&
test_line_count = 1 oid &&
- git tag twelfth-signed-alt $(cat oid)
+ git tag twelfth-signed-alt $(cat oid) &&
+
+ echo 13>file && test_tick && git commit -a -m thirteenth -S"${GPGSSH_KEY_ECDSA}" &&
+ git tag thirteenth-signed-ecdsa
+'
+
+test_expect_success GPGSSH 'sign commits using literal public keys with ssh-agent' '
+ test_when_finished "test_unconfig commit.gpgsign" &&
+ test_config gpg.format ssh &&
+ eval $(ssh-agent) &&
+ test_when_finished "kill ${SSH_AGENT_PID}" &&
+ ssh-add "${GPGSSH_KEY_PRIMARY}" &&
+ echo 1 >file && git add file &&
+ git commit -a -m rsa-inline -S"$(cat "${GPGSSH_KEY_PRIMARY}.pub")" &&
+ echo 2 >file &&
+ test_config user.signingkey "$(cat "${GPGSSH_KEY_PRIMARY}.pub")" &&
+ git commit -a -m rsa-config -S &&
+ ssh-add "${GPGSSH_KEY_ECDSA}" &&
+ echo 3 >file &&
+ git commit -a -m ecdsa-inline -S"key::$(cat "${GPGSSH_KEY_ECDSA}.pub")" &&
+ echo 4 >file &&
+ test_config user.signingkey "key::$(cat "${GPGSSH_KEY_ECDSA}.pub")" &&
+ git commit -a -m ecdsa-config -S
+'
+
+test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'create signed commits with keys having defined lifetimes' '
+ test_when_finished "test_unconfig commit.gpgsign" &&
+ test_config gpg.format ssh &&
+
+ echo expired >file && test_tick && git commit -a -m expired -S"${GPGSSH_KEY_EXPIRED}" &&
+ git tag expired-signed &&
+
+ echo notyetvalid >file && test_tick && git commit -a -m notyetvalid -S"${GPGSSH_KEY_NOTYETVALID}" &&
+ git tag notyetvalid-signed &&
+
+ echo timeboxedvalid >file && test_tick && git commit -a -m timeboxedvalid -S"${GPGSSH_KEY_TIMEBOXEDVALID}" &&
+ git tag timeboxedvalid-signed &&
+
+ echo timeboxedinvalid >file && test_tick && git commit -a -m timeboxedinvalid -S"${GPGSSH_KEY_TIMEBOXEDINVALID}" &&
+ git tag timeboxedinvalid-signed
'
test_expect_success GPGSSH 'verify and show signatures' '
@@ -122,6 +161,31 @@ test_expect_success GPGSSH 'verify-commit exits failure on untrusted signature'
grep "${GPGSSH_KEY_NOT_TRUSTED}" actual
'
+test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'verify-commit exits failure on expired signature key' '
+ test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
+ test_must_fail git verify-commit expired-signed 2>actual &&
+ ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
+'
+
+test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'verify-commit exits failure on not yet valid signature key' '
+ test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
+ test_must_fail git verify-commit notyetvalid-signed 2>actual &&
+ ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
+'
+
+test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'verify-commit succeeds with commit date and key validity matching' '
+ test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
+ git verify-commit timeboxedvalid-signed 2>actual &&
+ grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
+ ! grep "${GPGSSH_BAD_SIGNATURE}" actual
+'
+
+test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'verify-commit exits failure with commit date outside of key validity' '
+ test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
+ test_must_fail git verify-commit timeboxedinvalid-signed 2>actual &&
+ ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
+'
+
test_expect_success GPGSSH 'verify-commit exits success with matching minTrustLevel' '
test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
test_config gpg.minTrustLevel fully &&
@@ -217,7 +281,7 @@ test_expect_success GPGSSH 'amending already signed commit' '
test_config gpg.format ssh &&
test_config user.signingkey "${GPGSSH_KEY_PRIMARY}" &&
test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
- git checkout fourth-signed^0 &&
+ git checkout -f fourth-signed^0 &&
git commit --amend -S --no-edit &&
git verify-commit HEAD &&
git show -s --show-signature HEAD >actual &&
diff --git a/tag.c b/tag.c
index 3e18a41841..dfbcd7fcc2 100644
--- a/tag.c
+++ b/tag.c
@@ -25,8 +25,9 @@ static int run_gpg_verify(const char *buf, unsigned long size, unsigned flags)
return error("no signature found");
}
- ret = check_signature(payload.buf, payload.len, signature.buf,
- signature.len, &sigc);
+ sigc.payload_type = SIGNATURE_PAYLOAD_TAG;
+ sigc.payload = strbuf_detach(&payload, &sigc.payload_len);
+ ret = check_signature(&sigc, signature.buf, signature.len);
if (!(flags & GPG_VERIFY_OMIT_STATUS))
print_signature_buffer(&sigc, flags);
diff --git a/worktree.c b/worktree.c
index 2c155b1015..6f598dcfcd 100644
--- a/worktree.c
+++ b/worktree.c
@@ -404,17 +404,13 @@ int is_worktree_being_bisected(const struct worktree *wt,
* bisect). New commands that do similar things should update this
* function as well.
*/
-const struct worktree *find_shared_symref(const char *symref,
+const struct worktree *find_shared_symref(struct worktree **worktrees,
+ const char *symref,
const char *target)
{
const struct worktree *existing = NULL;
- static struct worktree **worktrees;
int i = 0;
- if (worktrees)
- free_worktrees(worktrees);
- worktrees = get_worktrees();
-
for (i = 0; worktrees[i]; i++) {
struct worktree *wt = worktrees[i];
const char *symref_target;
diff --git a/worktree.h b/worktree.h
index 8b7c408132..9e06fcbdf3 100644
--- a/worktree.h
+++ b/worktree.h
@@ -143,9 +143,10 @@ void free_worktrees(struct worktree **);
/*
* Check if a per-worktree symref points to a ref in the main worktree
* or any linked worktree, and return the worktree that holds the ref,
- * or NULL otherwise. The result may be destroyed by the next call.
+ * or NULL otherwise.
*/
-const struct worktree *find_shared_symref(const char *symref,
+const struct worktree *find_shared_symref(struct worktree **worktrees,
+ const char *symref,
const char *target);
/*
diff --git a/xdiff/xdiffi.c b/xdiff/xdiffi.c
index a4542c05b6..69689fab24 100644
--- a/xdiff/xdiffi.c
+++ b/xdiff/xdiffi.c
@@ -390,12 +390,9 @@ static xdchange_t *xdl_add_change(xdchange_t *xscr, long i1, long i2, long chg1,
}
-static int recs_match(xrecord_t *rec1, xrecord_t *rec2, long flags)
+static int recs_match(xrecord_t *rec1, xrecord_t *rec2)
{
- return (rec1->ha == rec2->ha &&
- xdl_recmatch(rec1->ptr, rec1->size,
- rec2->ptr, rec2->size,
- flags));
+ return (rec1->ha == rec2->ha);
}
/*
@@ -759,10 +756,10 @@ static inline int group_previous(xdfile_t *xdf, struct xdlgroup *g)
* following group, expand this group to include it. Return 0 on success or -1
* if g cannot be slid down.
*/
-static int group_slide_down(xdfile_t *xdf, struct xdlgroup *g, long flags)
+static int group_slide_down(xdfile_t *xdf, struct xdlgroup *g)
{
if (g->end < xdf->nrec &&
- recs_match(xdf->recs[g->start], xdf->recs[g->end], flags)) {
+ recs_match(xdf->recs[g->start], xdf->recs[g->end])) {
xdf->rchg[g->start++] = 0;
xdf->rchg[g->end++] = 1;
@@ -780,10 +777,10 @@ static int group_slide_down(xdfile_t *xdf, struct xdlgroup *g, long flags)
* into a previous group, expand this group to include it. Return 0 on success
* or -1 if g cannot be slid up.
*/
-static int group_slide_up(xdfile_t *xdf, struct xdlgroup *g, long flags)
+static int group_slide_up(xdfile_t *xdf, struct xdlgroup *g)
{
if (g->start > 0 &&
- recs_match(xdf->recs[g->start - 1], xdf->recs[g->end - 1], flags)) {
+ recs_match(xdf->recs[g->start - 1], xdf->recs[g->end - 1])) {
xdf->rchg[--g->start] = 1;
xdf->rchg[--g->end] = 0;
@@ -833,7 +830,7 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {
end_matching_other = -1;
/* Shift the group backward as much as possible: */
- while (!group_slide_up(xdf, &g, flags))
+ while (!group_slide_up(xdf, &g))
if (group_previous(xdfo, &go))
BUG("group sync broken sliding up");
@@ -848,7 +845,7 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {
/* Now shift the group forward as far as possible: */
while (1) {
- if (group_slide_down(xdf, &g, flags))
+ if (group_slide_down(xdf, &g))
break;
if (group_next(xdfo, &go))
BUG("group sync broken sliding down");
@@ -875,7 +872,7 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {
* other file that it can align with.
*/
while (go.end == go.start) {
- if (group_slide_up(xdf, &g, flags))
+ if (group_slide_up(xdf, &g))
BUG("match disappeared");
if (group_previous(xdfo, &go))
BUG("group sync broken sliding to match");
@@ -918,7 +915,7 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {
}
while (g.end > best_shift) {
- if (group_slide_up(xdf, &g, flags))
+ if (group_slide_up(xdf, &g))
BUG("best shift unreached");
if (group_previous(xdfo, &go))
BUG("group sync broken sliding to blank line");
diff --git a/xdiff/xhistogram.c b/xdiff/xhistogram.c
index e694bfd9e3..80794748b0 100644
--- a/xdiff/xhistogram.c
+++ b/xdiff/xhistogram.c
@@ -88,19 +88,14 @@ struct region {
#define REC(env, s, l) \
(env->xdf##s.recs[l - 1])
-static int cmp_recs(xpparam_t const *xpp,
- xrecord_t *r1, xrecord_t *r2)
+static int cmp_recs(xrecord_t *r1, xrecord_t *r2)
{
- return r1->ha == r2->ha &&
- xdl_recmatch(r1->ptr, r1->size, r2->ptr, r2->size,
- xpp->flags);
-}
+ return r1->ha == r2->ha;
-#define CMP_ENV(xpp, env, s1, l1, s2, l2) \
- (cmp_recs(xpp, REC(env, s1, l1), REC(env, s2, l2)))
+}
#define CMP(i, s1, l1, s2, l2) \
- (cmp_recs(i->xpp, REC(i->env, s1, l1), REC(i->env, s2, l2)))
+ (cmp_recs(REC(i->env, s1, l1), REC(i->env, s2, l2)))
#define TABLE_HASH(index, side, line) \
XDL_HASHLONG((REC(index->env, side, line))->ha, index->table_bits)
diff --git a/xdiff/xprepare.c b/xdiff/xprepare.c
index abeb8fb84e..4527a4a07c 100644
--- a/xdiff/xprepare.c
+++ b/xdiff/xprepare.c
@@ -181,15 +181,11 @@ static int xdl_prepare_ctx(unsigned int pass, mmfile_t *mf, long narec, xpparam_
if (!(recs = (xrecord_t **) xdl_malloc(narec * sizeof(xrecord_t *))))
goto abort;
- if (XDF_DIFF_ALG(xpp->flags) == XDF_HISTOGRAM_DIFF)
- hbits = hsize = 0;
- else {
- hbits = xdl_hashbits((unsigned int) narec);
- hsize = 1 << hbits;
- if (!(rhash = (xrecord_t **) xdl_malloc(hsize * sizeof(xrecord_t *))))
- goto abort;
- memset(rhash, 0, hsize * sizeof(xrecord_t *));
- }
+ hbits = xdl_hashbits((unsigned int) narec);
+ hsize = 1 << hbits;
+ if (!(rhash = (xrecord_t **) xdl_malloc(hsize * sizeof(xrecord_t *))))
+ goto abort;
+ memset(rhash, 0, hsize * sizeof(xrecord_t *));
nrec = 0;
if ((cur = blk = xdl_mmfile_first(mf, &bsize)) != NULL) {
@@ -208,9 +204,7 @@ static int xdl_prepare_ctx(unsigned int pass, mmfile_t *mf, long narec, xpparam_
crec->size = (long) (cur - prev);
crec->ha = hav;
recs[nrec++] = crec;
-
- if ((XDF_DIFF_ALG(xpp->flags) != XDF_HISTOGRAM_DIFF) &&
- xdl_classify_record(pass, cf, rhash, hbits, crec) < 0)
+ if (xdl_classify_record(pass, cf, rhash, hbits, crec) < 0)
goto abort;
}
}
@@ -219,10 +213,13 @@ static int xdl_prepare_ctx(unsigned int pass, mmfile_t *mf, long narec, xpparam_
goto abort;
memset(rchg, 0, (nrec + 2) * sizeof(char));
- if (!(rindex = (long *) xdl_malloc((nrec + 1) * sizeof(long))))
- goto abort;
- if (!(ha = (unsigned long *) xdl_malloc((nrec + 1) * sizeof(unsigned long))))
- goto abort;
+ if ((XDF_DIFF_ALG(xpp->flags) != XDF_PATIENCE_DIFF) &&
+ (XDF_DIFF_ALG(xpp->flags) != XDF_HISTOGRAM_DIFF)) {
+ if (!(rindex = xdl_malloc((nrec + 1) * sizeof(*rindex))))
+ goto abort;
+ if (!(ha = xdl_malloc((nrec + 1) * sizeof(*ha))))
+ goto abort;
+ }
xdf->nrec = nrec;
xdf->recs = recs;
@@ -279,8 +276,7 @@ int xdl_prepare_env(mmfile_t *mf1, mmfile_t *mf2, xpparam_t const *xpp,
enl1 = xdl_guess_lines(mf1, sample) + 1;
enl2 = xdl_guess_lines(mf2, sample) + 1;
- if (XDF_DIFF_ALG(xpp->flags) != XDF_HISTOGRAM_DIFF &&
- xdl_init_classifier(&cf, enl1 + enl2 + 1, xpp->flags) < 0)
+ if (xdl_init_classifier(&cf, enl1 + enl2 + 1, xpp->flags) < 0)
return -1;
if (xdl_prepare_ctx(1, mf1, enl1, xpp, &cf, &xe->xdf1) < 0) {
@@ -305,8 +301,7 @@ int xdl_prepare_env(mmfile_t *mf1, mmfile_t *mf2, xpparam_t const *xpp,
return -1;
}
- if (XDF_DIFF_ALG(xpp->flags) != XDF_HISTOGRAM_DIFF)
- xdl_free_classifier(&cf);
+ xdl_free_classifier(&cf);
return 0;
}