diff options
79 files changed, 3887 insertions, 396 deletions
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7bacb322e4..683a2d633e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -266,6 +266,9 @@ jobs: - jobname: linux-sha256 cc: clang pool: ubuntu-latest + - jobname: linux-reftable + cc: clang + pool: ubuntu-latest - jobname: linux-gcc cc: gcc cc_package: gcc-8 @@ -277,6 +280,9 @@ jobs: - jobname: osx-clang cc: clang pool: macos-13 + - jobname: osx-reftable + cc: clang + pool: macos-13 - jobname: osx-gcc cc: gcc cc_package: gcc-13 @@ -287,6 +293,9 @@ jobs: - jobname: linux-leaks cc: gcc pool: ubuntu-latest + - jobname: linux-reftable-leaks + cc: gcc + pool: ubuntu-latest - jobname: linux-asan-ubsan cc: clang pool: ubuntu-latest diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 43bfbd8834..c0fa2fe90b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -26,6 +26,9 @@ test:linux: - jobname: linux-sha256 image: ubuntu:latest CC: clang + - jobname: linux-reftable + image: ubuntu:latest + CC: clang - jobname: linux-gcc image: ubuntu:20.04 CC: gcc @@ -40,6 +43,9 @@ test:linux: - jobname: linux-leaks image: ubuntu:latest CC: gcc + - jobname: linux-reftable-leaks + image: ubuntu:latest + CC: gcc - jobname: linux-asan-ubsan image: ubuntu:latest CC: clang @@ -79,6 +85,9 @@ test:osx: - jobname: osx-clang image: macos-13-xcode-14 CC: clang + - jobname: osx-reftable + image: macos-13-xcode-14 + CC: clang artifacts: paths: - t/failed-test-artifacts diff --git a/Documentation/RelNotes/2.43.3.txt b/Documentation/RelNotes/2.43.3.txt new file mode 100644 index 0000000000..924f20594f --- /dev/null +++ b/Documentation/RelNotes/2.43.3.txt @@ -0,0 +1,12 @@ +Git 2.43.3 Release Notes +======================== + +Relative to Git 2.43.2, this release fixes one regression that +manifests while running "git commit -v --trailer". + +Fixes since Git 2.43.2 +---------------------- + + * "git commit -v --trailer=..." was broken with recent update and + placed the trailer _after_ the divider line, which has been + corrected. diff --git a/Documentation/RelNotes/2.45.0.txt b/Documentation/RelNotes/2.45.0.txt new file mode 100644 index 0000000000..e75e5ca362 --- /dev/null +++ b/Documentation/RelNotes/2.45.0.txt @@ -0,0 +1,61 @@ +Git v2.45 Release Notes +======================= + +Backward Compatibility Notes + +UI, Workflows & Features + + * Integrate the reftable code into the refs framework as a backend. + With "git init --ref-format=reftable", hopefully it would be a lot + more efficient to manage a repository with many references. + + * "git checkout -p" and friends learned that that "@" is a synonym + for "HEAD". + + * Variants of vimdiff learned to honor mergetool.<variant>.layout + settings. + + +Performance, Internal Implementation, Development Support etc. + + * The code to iterate over refs with the reftable backend has seen + some optimization. + + * More tests that are marked as "ref-files only" have been updated to + improve test coverage of reftable backend. + + * Some parts of command line completion script (in contrib/) have + been micro-optimized. + + +Fixes since v2.44 +----------------- + + * "git apply" on a filesystem without filemode support have learned + to take a hint from what is in the index for the path, even when + not working with the "--index" or "--cached" option, when checking + the executable bit match what is required by the preimage in the + patch. + (merge 45b625142d cp/apply-core-filemode later to maint). + + * "git column" has been taught to reject negative padding value, as + it would lead to nonsense behaviour including division by zero. + (merge 76fb807faa kh/column-reject-negative-padding later to maint). + + * "git am --help" now tells readers what actions are available in + "git am --whitespace=<action>", in addition to saying that the + option is passed through to the underlying "git apply". + (merge a171dac734 jc/am-whitespace-doc later to maint). + + * "git tag --column" failed to check the exit status of its "git + column" invocation, which has been corrected. + (merge 92e66478fc rj/tag-column-fix later to maint). + + * Credential helper based on libsecret (in contrib/) has been updated + to handle an empty password correctly. + (merge 8f1f2023b7 mh/libsecret-empty-password-fix later to maint). + + * Other code cleanup, docfix, build fix, etc. + (merge f0e578c69c rs/use-xstrncmpz later to maint). + (merge 83e6eb7d7a ba/credential-test-clean-fix later to maint). + (merge 64562d784d jb/doc-interactive-singlekey-do-not-need-perl later to maint). diff --git a/Documentation/config/interactive.txt b/Documentation/config/interactive.txt index a2d3c7ec44..5cc26555f1 100644 --- a/Documentation/config/interactive.txt +++ b/Documentation/config/interactive.txt @@ -4,9 +4,7 @@ interactive.singleKey:: Currently this is used by the `--patch` mode of linkgit:git-add[1], linkgit:git-checkout[1], linkgit:git-restore[1], linkgit:git-commit[1], - linkgit:git-reset[1], and linkgit:git-stash[1]. Note that this - setting is silently ignored if portable keystroke input - is not available; requires the Perl module Term::ReadKey. + linkgit:git-reset[1], and linkgit:git-stash[1]. interactive.diffFilter:: When an interactive command (such as `git add --patch`) shows diff --git a/Documentation/config/mergetool.txt b/Documentation/config/mergetool.txt index 294f61efd1..00bf665aa0 100644 --- a/Documentation/config/mergetool.txt +++ b/Documentation/config/mergetool.txt @@ -45,14 +45,21 @@ mergetool.meld.useAutoMerge:: value of `false` avoids using `--auto-merge` altogether, and is the default value. -mergetool.vimdiff.layout:: - The vimdiff backend uses this variable to control how its split - windows appear. Applies even if you are using Neovim (`nvim`) or - gVim (`gvim`) as the merge tool. See BACKEND SPECIFIC HINTS section +mergetool.<vimdiff variant>.layout:: + Configure the split window layout for vimdiff's `<variant>`, which is any of `vimdiff`, + `nvimdiff`, `gvimdiff`. + Upon launching `git mergetool` with `--tool=<variant>` (or without `--tool` + if `merge.tool` is configured as `<variant>`), Git will consult + `mergetool.<variant>.layout` to determine the tool's layout. If the + variant-specific configuration is not available, `vimdiff`'s is used as + fallback. If that too is not available, a default layout with 4 windows + will be used. To configure the layout, see the `BACKEND SPECIFIC HINTS` +ifdef::git-mergetool[] + section. +endif::[] ifndef::git-mergetool[] - in linkgit:git-mergetool[1]. + section in linkgit:git-mergetool[1]. endif::[] - for details. mergetool.hideResolved:: During a merge, Git will automatically resolve as many conflicts as diff --git a/Documentation/git-am.txt b/Documentation/git-am.txt index e080458d6c..463a3c6600 100644 --- a/Documentation/git-am.txt +++ b/Documentation/git-am.txt @@ -128,6 +128,9 @@ include::rerere-options.txt[] These flags are passed to the 'git apply' (see linkgit:git-apply[1]) program that applies the patch. ++ +Valid <action> for the `--whitespace` option are: +`nowarn`, `warn`, `fix`, `error`, and `error-all`. --patch-format:: By default the command will try to detect the patch format diff --git a/Documentation/git-difftool.txt b/Documentation/git-difftool.txt index c05f97aca9..a616f8b2e6 100644 --- a/Documentation/git-difftool.txt +++ b/Documentation/git-difftool.txt @@ -105,7 +105,6 @@ instead. `--no-symlinks` is the default on Windows. `merge.tool` until a tool is found. --[no-]trust-exit-code:: - 'git-difftool' invokes a diff tool individually on each file. Errors reported by the diff tool are ignored by default. Use `--trust-exit-code` to make 'git-difftool' exit when an invoked diff tool returns a non-zero exit code. diff --git a/Documentation/mergetools/vimdiff.txt b/Documentation/mergetools/vimdiff.txt index d1a4c468e6..befa86d692 100644 --- a/Documentation/mergetools/vimdiff.txt +++ b/Documentation/mergetools/vimdiff.txt @@ -177,7 +177,8 @@ Instead of `--tool=vimdiff`, you can also use one of these other variants: When using these variants, in order to specify a custom layout you will have to set configuration variables `mergetool.gvimdiff.layout` and -`mergetool.nvimdiff.layout` instead of `mergetool.vimdiff.layout` +`mergetool.nvimdiff.layout` instead of `mergetool.vimdiff.layout` (though the +latter will be used as fallback if the variant-specific one is not set). In addition, for backwards compatibility with previous Git versions, you can also append `1`, `2` or `3` to either `vimdiff` or any of the variants (ex: diff --git a/Documentation/ref-storage-format.txt b/Documentation/ref-storage-format.txt index 1a65cac468..14fff8a9c6 100644 --- a/Documentation/ref-storage-format.txt +++ b/Documentation/ref-storage-format.txt @@ -1 +1,3 @@ * `files` for loose files with packed-refs. This is the default. +* `reftable` for the reftable format. This format is experimental and its + internals are subject to change. diff --git a/Documentation/technical/repository-version.txt b/Documentation/technical/repository-version.txt index 27be3741e6..47281420fc 100644 --- a/Documentation/technical/repository-version.txt +++ b/Documentation/technical/repository-version.txt @@ -103,5 +103,6 @@ GIT_COMMON_DIR/worktrees/<id>/config.worktree) ==== `refStorage` -Specifies the file format for the ref database. The only valid value -is `files` (loose references with a packed-refs file). +Specifies the file format for the ref database. The valid values are +`files` (loose references with a packed-refs file) and `reftable` (see +Documentation/technical/reftable.txt). diff --git a/GIT-VERSION-GEN b/GIT-VERSION-GEN index 6339941f8b..c9d1d29082 100755 --- a/GIT-VERSION-GEN +++ b/GIT-VERSION-GEN @@ -1,7 +1,7 @@ #!/bin/sh GVF=GIT-VERSION-FILE -DEF_VER=v2.44.0-rc2 +DEF_VER=v2.44.0 LF=' ' @@ -1126,6 +1126,7 @@ LIB_OBJS += reflog.o LIB_OBJS += refs.o LIB_OBJS += refs/debug.o LIB_OBJS += refs/files-backend.o +LIB_OBJS += refs/reftable-backend.o LIB_OBJS += refs/iterator.o LIB_OBJS += refs/packed-backend.o LIB_OBJS += refs/ref-cache.o @@ -1 +1 @@ -Documentation/RelNotes/2.44.0.txt
\ No newline at end of file +Documentation/RelNotes/2.45.0.txt
\ No newline at end of file diff --git a/add-patch.c b/add-patch.c index 79eda168eb..68f525b35c 100644 --- a/add-patch.c +++ b/add-patch.c @@ -1729,14 +1729,6 @@ int run_add_p(struct repository *r, enum add_p_mode mode, if (mode == ADD_P_STASH) s.mode = &patch_mode_stash; else if (mode == ADD_P_RESET) { - /* - * NEEDSWORK: Instead of comparing to the literal "HEAD", - * compare the commit objects instead so that other ways of - * saying the same thing (such as "@") are also handled - * appropriately. - * - * This applies to the cases below too. - */ if (!revision || !strcmp(revision, "HEAD")) s.mode = &patch_mode_reset_head; else @@ -77,7 +77,8 @@ static int parse_whitespace_option(struct apply_state *state, const char *option return 0; } /* - * Please update $__git_whitespacelist in git-completion.bash + * Please update $__git_whitespacelist in git-completion.bash, + * Documentation/git-apply.txt, and Documentation/git-am.txt * when you add new options. */ return error(_("unrecognized whitespace option '%s'"), option); @@ -2219,7 +2220,8 @@ static void reverse_patches(struct patch *p) struct fragment *frag = p->fragments; SWAP(p->new_name, p->old_name); - SWAP(p->new_mode, p->old_mode); + if (p->new_mode) + SWAP(p->new_mode, p->old_mode); SWAP(p->is_new, p->is_delete); SWAP(p->lines_added, p->lines_deleted); SWAP(p->old_oid_prefix, p->new_oid_prefix); @@ -3777,8 +3779,17 @@ static int check_preimage(struct apply_state *state, return error_errno("%s", old_name); } - if (!state->cached && !previous) - st_mode = ce_mode_from_stat(*ce, st->st_mode); + if (!state->cached && !previous) { + if (*ce && !(*ce)->ce_mode) + BUG("ce_mode == 0 for path '%s'", old_name); + + if (trust_executable_bit) + st_mode = ce_mode_from_stat(*ce, st->st_mode); + else if (*ce) + st_mode = (*ce)->ce_mode; + else + st_mode = patch->old_mode; + } if (patch->is_new < 0) patch->is_new = 0; diff --git a/archive-tar.c b/archive-tar.c index f2a0ed7752..8ae30125f8 100644 --- a/archive-tar.c +++ b/archive-tar.c @@ -365,7 +365,7 @@ static struct archiver *find_tar_filter(const char *name, size_t len) int i; for (i = 0; i < nr_tar_filters; i++) { struct archiver *ar = tar_filters[i]; - if (!strncmp(ar->name, name, len) && !ar->name[len]) + if (!xstrncmpz(ar->name, name, len)) return ar; } return NULL; diff --git a/builtin/checkout.c b/builtin/checkout.c index a6e30931b5..067c251933 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -1224,7 +1224,9 @@ static void setup_new_branch_info_and_source_tree( struct tree **source_tree = &opts->source_tree; struct object_id branch_rev; - new_branch_info->name = xstrdup(arg); + /* treat '@' as a shortcut for 'HEAD' */ + new_branch_info->name = !strcmp(arg, "@") ? xstrdup("HEAD") : + xstrdup(arg); setup_branch_path(new_branch_info); if (!check_refname_format(new_branch_info->path, 0) && diff --git a/builtin/column.c b/builtin/column.c index e80218f81f..10ff7e0166 100644 --- a/builtin/column.c +++ b/builtin/column.c @@ -45,6 +45,8 @@ int cmd_column(int argc, const char **argv, const char *prefix) memset(&copts, 0, sizeof(copts)); copts.padding = 1; argc = parse_options(argc, argv, prefix, options, builtin_column_usage, 0); + if (copts.padding < 0) + die(_("%s must be non-negative"), "--padding"); if (argc) usage_with_options(builtin_column_usage, options); if (real_command || command) { diff --git a/builtin/fast-export.c b/builtin/fast-export.c index f18f0809f9..4693d18cc9 100644 --- a/builtin/fast-export.c +++ b/builtin/fast-export.c @@ -136,8 +136,7 @@ static int anonymized_entry_cmp(const void *cmp_data UNUSED, a = container_of(eptr, const struct anonymized_entry, hash); if (keydata) { const struct anonymized_entry_key *key = keydata; - int equal = !strncmp(a->orig, key->orig, key->orig_len) && - !a->orig[key->orig_len]; + int equal = !xstrncmpz(a->orig, key->orig, key->orig_len); return !equal; } diff --git a/builtin/merge.c b/builtin/merge.c index 8f819781cc..935c8a57dd 100644 --- a/builtin/merge.c +++ b/builtin/merge.c @@ -192,8 +192,7 @@ static struct strategy *get_strategy(const char *name) int j, found = 0; struct cmdname *ent = main_cmds.names[i]; for (j = 0; !found && j < ARRAY_SIZE(all_strategy); j++) - if (!strncmp(ent->name, all_strategy[j].name, ent->len) - && !all_strategy[j].name[ent->len]) + if (!xstrncmpz(all_strategy[j].name, ent->name, ent->len)) found = 1; if (!found) add_cmdname(¬_strategies, ent->name, ent->len); diff --git a/builtin/reflog.c b/builtin/reflog.c index a5a4099f61..2c3369fca5 100644 --- a/builtin/reflog.c +++ b/builtin/reflog.c @@ -96,8 +96,7 @@ static struct reflog_expire_cfg *find_cfg_ent(const char *pattern, size_t len) reflog_expire_cfg_tail = &reflog_expire_cfg; for (ent = reflog_expire_cfg; ent; ent = ent->next) - if (!strncmp(ent->pattern, pattern, len) && - ent->pattern[len] == '\0') + if (!xstrncmpz(ent->pattern, pattern, len)) return ent; FLEX_ALLOC_MEM(ent, pattern, pattern, len); diff --git a/builtin/reset.c b/builtin/reset.c index 8390bfe4c4..f0bf29a478 100644 --- a/builtin/reset.c +++ b/builtin/reset.c @@ -281,7 +281,9 @@ static void parse_args(struct pathspec *pathspec, verify_filename(prefix, argv[0], 1); } } - *rev_ret = rev; + + /* treat '@' as a shortcut for 'HEAD' */ + *rev_ret = !strcmp("@", rev) ? "HEAD" : rev; parse_pathspec(pathspec, 0, PATHSPEC_PREFER_FULL | diff --git a/builtin/tag.c b/builtin/tag.c index 37473ac21f..19a7e06bf4 100644 --- a/builtin/tag.c +++ b/builtin/tag.c @@ -530,7 +530,8 @@ int cmd_tag(int argc, const char **argv, const char *prefix) struct column_options copts; memset(&copts, 0, sizeof(copts)); copts.padding = 2; - run_column_filter(colopts, &copts); + if (run_column_filter(colopts, &copts)) + die(_("could not start 'git column'")); } filter.name_patterns = argv; ret = list_tags(&filter, sorting, &format); @@ -367,7 +367,7 @@ linux-musl) MAKEFLAGS="$MAKEFLAGS NO_REGEX=Yes ICONV_OMITS_BOM=Yes" MAKEFLAGS="$MAKEFLAGS GIT_TEST_UTF8_LOCALE=C.UTF-8" ;; -linux-leaks) +linux-leaks|linux-reftable-leaks) export SANITIZE=leak export GIT_TEST_PASSING_SANITIZE_LEAK=true export GIT_TEST_SANITIZE_LEAK_LOG=true diff --git a/ci/run-build-and-tests.sh b/ci/run-build-and-tests.sh index 7a1466b868..c192bd613c 100755 --- a/ci/run-build-and-tests.sh +++ b/ci/run-build-and-tests.sh @@ -37,6 +37,9 @@ linux-clang) linux-sha256) export GIT_TEST_DEFAULT_HASH=sha256 ;; +linux-reftable|linux-reftable-leaks|osx-reftable) + export GIT_TEST_DEFAULT_REF_FORMAT=reftable + ;; pedantic) # Don't run the tests; we only care about whether Git can be # built. @@ -182,6 +182,8 @@ void print_columns(const struct string_list *list, unsigned int colopts, { struct column_options nopts; + if (opts && (0 > opts->padding)) + BUG("padding must be non-negative"); if (!list->nr) return; assert((colopts & COL_ENABLE_MASK) != COL_AUTO); @@ -361,6 +363,8 @@ int run_column_filter(int colopts, const struct column_options *opts) { struct strvec *argv; + if (opts && (0 > opts->padding)) + BUG("padding must be non-negative"); if (fd_out != -1) return -1; diff --git a/contrib/coccinelle/xstrncmpz.cocci b/contrib/coccinelle/xstrncmpz.cocci new file mode 100644 index 0000000000..ccb39e2bc0 --- /dev/null +++ b/contrib/coccinelle/xstrncmpz.cocci @@ -0,0 +1,28 @@ +@@ +expression S, T, L; +@@ +( +- strncmp(S, T, L) || S[L] ++ !!xstrncmpz(S, T, L) +| +- strncmp(S, T, L) || S[L] != '\0' ++ !!xstrncmpz(S, T, L) +| +- strncmp(S, T, L) || T[L] ++ !!xstrncmpz(T, S, L) +| +- strncmp(S, T, L) || T[L] != '\0' ++ !!xstrncmpz(T, S, L) +| +- !strncmp(S, T, L) && !S[L] ++ !xstrncmpz(S, T, L) +| +- !strncmp(S, T, L) && S[L] == '\0' ++ !xstrncmpz(S, T, L) +| +- !strncmp(S, T, L) && !T[L] ++ !xstrncmpz(T, S, L) +| +- !strncmp(S, T, L) && T[L] == '\0' ++ !xstrncmpz(T, S, L) +) diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index 444b3efa63..fcf1afd75d 100644 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -2673,7 +2673,8 @@ __git_compute_first_level_config_vars_for_section () __git_compute_config_vars local this_section="__git_first_level_config_vars_for_section_${section}" test -n "${!this_section}" || - printf -v "__git_first_level_config_vars_for_section_${section}" %s "$(echo "$__git_config_vars" | grep -E "^${section}\.[a-z]" | awk -F. '{print $2}')" + printf -v "__git_first_level_config_vars_for_section_${section}" %s \ + "$(echo "$__git_config_vars" | awk -F. "/^${section}\.[a-z]/ { print \$2 }")" } __git_compute_second_level_config_vars_for_section () @@ -2682,7 +2683,8 @@ __git_compute_second_level_config_vars_for_section () __git_compute_config_vars_all local this_section="__git_second_level_config_vars_for_section_${section}" test -n "${!this_section}" || - printf -v "__git_second_level_config_vars_for_section_${section}" %s "$(echo "$__git_config_vars_all" | grep -E "^${section}\.<" | awk -F. '{print $3}')" + printf -v "__git_second_level_config_vars_for_section_${section}" %s \ + "$(echo "$__git_config_vars_all" | awk -F. "/^${section}\.</ { print \$3 }")" } __git_config_sections= diff --git a/contrib/credential/libsecret/git-credential-libsecret.c b/contrib/credential/libsecret/git-credential-libsecret.c index 215a81d8ba..90034d0cf1 100644 --- a/contrib/credential/libsecret/git-credential-libsecret.c +++ b/contrib/credential/libsecret/git-credential-libsecret.c @@ -164,6 +164,9 @@ static int keyring_get(struct credential *c) if (g_strv_length(parts) >= 1) { g_free(c->password); c->password = g_strdup(parts[0]); + } else { + g_free(c->password); + c->password = g_strdup(""); } for (int i = 1; i < g_strv_length(parts); i++) { if (g_str_has_prefix(parts[i], "password_expiry_utc=")) { diff --git a/contrib/workdir/git-new-workdir b/contrib/workdir/git-new-workdir index 888c34a521..989197aace 100755 --- a/contrib/workdir/git-new-workdir +++ b/contrib/workdir/git-new-workdir @@ -79,7 +79,7 @@ trap cleanup $siglist # create the links to the original repo. explicitly exclude index, HEAD and # logs/HEAD from the list since they are purely related to the current working # directory, and should not be shared. -for x in config refs logs/refs objects info hooks packed-refs remotes rr-cache svn +for x in config refs logs/refs objects info hooks packed-refs remotes rr-cache svn reftable do # create a containing directory if needed case $x in @@ -1028,7 +1028,7 @@ static int read_convert_config(const char *var, const char *value, if (parse_config_key(var, "filter", &name, &namelen, &key) < 0 || !name) return 0; for (drv = user_convert; drv; drv = drv->next) - if (!strncmp(drv->name, name, namelen) && !drv->name[namelen]) + if (!xstrncmpz(drv->name, name, namelen)) break; if (!drv) { CALLOC_ARRAY(drv, 1); diff --git a/git-difftool--helper.sh b/git-difftool--helper.sh index e4e820e680..dd0c9a5b7f 100755 --- a/git-difftool--helper.sh +++ b/git-difftool--helper.sh @@ -91,6 +91,19 @@ then # ignore the error from the above --- run_merge_tool # will diagnose unusable tool by itself run_merge_tool "$merge_tool" false + + status=$? + if test $status -ge 126 + then + # Command not found (127), not executable (126) or + # exited via a signal (>= 128). + exit $status + fi + + if test "$GIT_DIFFTOOL_TRUST_EXIT_CODE" = true + then + exit $status + fi else # Launch the merge tool on each path provided by 'git diff' while test $# -gt 6 diff --git a/merge-ll.c b/merge-ll.c index 5ffb045efb..61e0ae5398 100644 --- a/merge-ll.c +++ b/merge-ll.c @@ -292,7 +292,7 @@ static int read_merge_config(const char *var, const char *value, * after seeing merge.<name>.var1. */ for (fn = ll_user_merge; fn; fn = fn->next) - if (!strncmp(fn->name, name, namelen) && !fn->name[namelen]) + if (!xstrncmpz(fn->name, name, namelen)) break; if (!fn) { CALLOC_ARRAY(fn, 1); diff --git a/mergetools/vimdiff b/mergetools/vimdiff index 06937acbf5..97e376329b 100644 --- a/mergetools/vimdiff +++ b/mergetools/vimdiff @@ -371,9 +371,17 @@ diff_cmd_help () { merge_cmd () { - layout=$(git config mergetool.vimdiff.layout) + TOOL=$1 - case "$1" in + layout=$(git config "mergetool.$TOOL.layout") + + # backward compatibility: + if test -z "$layout" + then + layout=$(git config mergetool.vimdiff.layout) + fi + + case "$TOOL" in *vimdiff) if test -z "$layout" then @@ -47,8 +47,7 @@ int type_from_string_gently(const char *str, ssize_t len, int gentle) len = strlen(str); for (i = 1; i < ARRAY_SIZE(object_type_strings); i++) - if (!strncmp(str, object_type_strings[i], len) && - object_type_strings[i][len] == '\0') + if (!xstrncmpz(object_type_strings[i], str, len)) return i; if (gentle) @@ -871,7 +871,7 @@ const char *enter_repo(const char *path, int strict) return NULL; } -static int calc_shared_perm(int mode) +int calc_shared_perm(int mode) { int tweak; @@ -181,6 +181,7 @@ const char *git_path_shallow(struct repository *r); int ends_with_path_components(const char *path, const char *components); int validate_headref(const char *ref); +int calc_shared_perm(int mode); int adjust_shared_perm(const char *path); char *interpolate_path(const char *path, int real_home); @@ -35,6 +35,7 @@ */ static const struct ref_storage_be *refs_backends[] = { [REF_STORAGE_FORMAT_FILES] = &refs_be_files, + [REF_STORAGE_FORMAT_REFTABLE] = &refs_be_reftable, }; static const struct ref_storage_be *find_ref_storage_backend(unsigned int ref_storage_format) diff --git a/refs/refs-internal.h b/refs/refs-internal.h index 82219829b0..83e0f0bba3 100644 --- a/refs/refs-internal.h +++ b/refs/refs-internal.h @@ -693,6 +693,7 @@ struct ref_storage_be { }; extern struct ref_storage_be refs_be_files; +extern struct ref_storage_be refs_be_reftable; extern struct ref_storage_be refs_be_packed; /* diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c new file mode 100644 index 0000000000..a14f2ad7f4 --- /dev/null +++ b/refs/reftable-backend.c @@ -0,0 +1,2297 @@ +#include "../git-compat-util.h" +#include "../abspath.h" +#include "../chdir-notify.h" +#include "../environment.h" +#include "../gettext.h" +#include "../hash.h" +#include "../hex.h" +#include "../iterator.h" +#include "../ident.h" +#include "../lockfile.h" +#include "../object.h" +#include "../path.h" +#include "../refs.h" +#include "../reftable/reftable-stack.h" +#include "../reftable/reftable-record.h" +#include "../reftable/reftable-error.h" +#include "../reftable/reftable-iterator.h" +#include "../reftable/reftable-merged.h" +#include "../setup.h" +#include "../strmap.h" +#include "refs-internal.h" + +/* + * Used as a flag in ref_update::flags when the ref_update was via an + * update to HEAD. + */ +#define REF_UPDATE_VIA_HEAD (1 << 8) + +struct reftable_ref_store { + struct ref_store base; + + /* + * The main stack refers to the common dir and thus contains common + * refs as well as refs of the main repository. + */ + struct reftable_stack *main_stack; + /* + * The worktree stack refers to the gitdir in case the refdb is opened + * via a worktree. It thus contains the per-worktree refs. + */ + struct reftable_stack *worktree_stack; + /* + * Map of worktree stacks by their respective worktree names. The map + * is populated lazily when we try to resolve `worktrees/$worktree` refs. + */ + struct strmap worktree_stacks; + struct reftable_write_options write_options; + + unsigned int store_flags; + int err; +}; + +/* + * Downcast ref_store to reftable_ref_store. Die if ref_store is not a + * reftable_ref_store. required_flags is compared with ref_store's store_flags + * to ensure the ref_store has all required capabilities. "caller" is used in + * any necessary error messages. + */ +static struct reftable_ref_store *reftable_be_downcast(struct ref_store *ref_store, + unsigned int required_flags, + const char *caller) +{ + struct reftable_ref_store *refs; + + if (ref_store->be != &refs_be_reftable) + BUG("ref_store is type \"%s\" not \"reftables\" in %s", + ref_store->be->name, caller); + + refs = (struct reftable_ref_store *)ref_store; + + if ((refs->store_flags & required_flags) != required_flags) + BUG("operation %s requires abilities 0x%x, but only have 0x%x", + caller, required_flags, refs->store_flags); + + return refs; +} + +/* + * Some refs are global to the repository (refs/heads/{*}), while others are + * local to the worktree (eg. HEAD, refs/bisect/{*}). We solve this by having + * multiple separate databases (ie. multiple reftable/ directories), one for + * the shared refs, one for the current worktree refs, and one for each + * additional worktree. For reading, we merge the view of both the shared and + * the current worktree's refs, when necessary. + * + * This function also optionally assigns the rewritten reference name that is + * local to the stack. This translation is required when using worktree refs + * like `worktrees/$worktree/refs/heads/foo` as worktree stacks will store + * those references in their normalized form. + */ +static struct reftable_stack *stack_for(struct reftable_ref_store *store, + const char *refname, + const char **rewritten_ref) +{ + const char *wtname; + int wtname_len; + + if (!refname) + return store->main_stack; + + switch (parse_worktree_ref(refname, &wtname, &wtname_len, rewritten_ref)) { + case REF_WORKTREE_OTHER: { + static struct strbuf wtname_buf = STRBUF_INIT; + struct strbuf wt_dir = STRBUF_INIT; + struct reftable_stack *stack; + + /* + * We're using a static buffer here so that we don't need to + * allocate the worktree name whenever we look up a reference. + * This could be avoided if the strmap interface knew how to + * handle keys with a length. + */ + strbuf_reset(&wtname_buf); + strbuf_add(&wtname_buf, wtname, wtname_len); + + /* + * There is an edge case here: when the worktree references the + * current worktree, then we set up the stack once via + * `worktree_stacks` and once via `worktree_stack`. This is + * wasteful, but in the reading case it shouldn't matter. And + * in the writing case we would notice that the stack is locked + * already and error out when trying to write a reference via + * both stacks. + */ + stack = strmap_get(&store->worktree_stacks, wtname_buf.buf); + if (!stack) { + strbuf_addf(&wt_dir, "%s/worktrees/%s/reftable", + store->base.repo->commondir, wtname_buf.buf); + + store->err = reftable_new_stack(&stack, wt_dir.buf, + store->write_options); + assert(store->err != REFTABLE_API_ERROR); + strmap_put(&store->worktree_stacks, wtname_buf.buf, stack); + } + + strbuf_release(&wt_dir); + return stack; + } + case REF_WORKTREE_CURRENT: + /* + * If there is no worktree stack then we're currently in the + * main worktree. We thus return the main stack in that case. + */ + if (!store->worktree_stack) + return store->main_stack; + return store->worktree_stack; + case REF_WORKTREE_MAIN: + case REF_WORKTREE_SHARED: + return store->main_stack; + default: + BUG("unhandled worktree reference type"); + } +} + +static int should_write_log(struct ref_store *refs, const char *refname) +{ + if (log_all_ref_updates == LOG_REFS_UNSET) + log_all_ref_updates = is_bare_repository() ? LOG_REFS_NONE : LOG_REFS_NORMAL; + + switch (log_all_ref_updates) { + case LOG_REFS_NONE: + return refs_reflog_exists(refs, refname); + case LOG_REFS_ALWAYS: + return 1; + case LOG_REFS_NORMAL: + if (should_autocreate_reflog(refname)) + return 1; + return refs_reflog_exists(refs, refname); + default: + BUG("unhandled core.logAllRefUpdates value %d", log_all_ref_updates); + } +} + +static void clear_reftable_log_record(struct reftable_log_record *log) +{ + switch (log->value_type) { + case REFTABLE_LOG_UPDATE: + /* + * When we write log records, the hashes are owned by the + * caller and thus shouldn't be free'd. + */ + log->value.update.old_hash = NULL; + log->value.update.new_hash = NULL; + break; + case REFTABLE_LOG_DELETION: + break; + } + reftable_log_record_release(log); +} + +static void fill_reftable_log_record(struct reftable_log_record *log) +{ + const char *info = git_committer_info(0); + struct ident_split split = {0}; + int sign = 1; + + if (split_ident_line(&split, info, strlen(info))) + BUG("failed splitting committer info"); + + reftable_log_record_release(log); + log->value_type = REFTABLE_LOG_UPDATE; + log->value.update.name = + xstrndup(split.name_begin, split.name_end - split.name_begin); + log->value.update.email = + xstrndup(split.mail_begin, split.mail_end - split.mail_begin); + log->value.update.time = atol(split.date_begin); + if (*split.tz_begin == '-') { + sign = -1; + split.tz_begin++; + } + if (*split.tz_begin == '+') { + sign = 1; + split.tz_begin++; + } + + log->value.update.tz_offset = sign * atoi(split.tz_begin); +} + +static int read_ref_without_reload(struct reftable_stack *stack, + const char *refname, + struct object_id *oid, + struct strbuf *referent, + unsigned int *type) +{ + struct reftable_ref_record ref = {0}; + int ret; + + ret = reftable_stack_read_ref(stack, refname, &ref); + if (ret) + goto done; + + if (ref.value_type == REFTABLE_REF_SYMREF) { + strbuf_reset(referent); + strbuf_addstr(referent, ref.value.symref); + *type |= REF_ISSYMREF; + } else if (reftable_ref_record_val1(&ref)) { + oidread(oid, reftable_ref_record_val1(&ref)); + } else { + /* We got a tombstone, which should not happen. */ + BUG("unhandled reference value type %d", ref.value_type); + } + +done: + assert(ret != REFTABLE_API_ERROR); + reftable_ref_record_release(&ref); + return ret; +} + +static struct ref_store *reftable_be_init(struct repository *repo, + const char *gitdir, + unsigned int store_flags) +{ + struct reftable_ref_store *refs = xcalloc(1, sizeof(*refs)); + struct strbuf path = STRBUF_INIT; + int is_worktree; + mode_t mask; + + mask = umask(0); + umask(mask); + + base_ref_store_init(&refs->base, repo, gitdir, &refs_be_reftable); + strmap_init(&refs->worktree_stacks); + refs->store_flags = store_flags; + refs->write_options.block_size = 4096; + refs->write_options.hash_id = repo->hash_algo->format_id; + refs->write_options.default_permissions = calc_shared_perm(0666 & ~mask); + + /* + * Set up the main reftable stack that is hosted in GIT_COMMON_DIR. + * This stack contains both the shared and the main worktree refs. + * + * Note that we don't try to resolve the path in case we have a + * worktree because `get_common_dir_noenv()` already does it for us. + */ + is_worktree = get_common_dir_noenv(&path, gitdir); + if (!is_worktree) { + strbuf_reset(&path); + strbuf_realpath(&path, gitdir, 0); + } + strbuf_addstr(&path, "/reftable"); + refs->err = reftable_new_stack(&refs->main_stack, path.buf, + refs->write_options); + if (refs->err) + goto done; + + /* + * If we're in a worktree we also need to set up the worktree reftable + * stack that is contained in the per-worktree GIT_DIR. + * + * Ideally, we would also add the stack to our worktree stack map. But + * we have no way to figure out the worktree name here and thus can't + * do it efficiently. + */ + if (is_worktree) { + strbuf_reset(&path); + strbuf_addf(&path, "%s/reftable", gitdir); + + refs->err = reftable_new_stack(&refs->worktree_stack, path.buf, + refs->write_options); + if (refs->err) + goto done; + } + + chdir_notify_reparent("reftables-backend $GIT_DIR", &refs->base.gitdir); + +done: + assert(refs->err != REFTABLE_API_ERROR); + strbuf_release(&path); + return &refs->base; +} + +static int reftable_be_init_db(struct ref_store *ref_store, + int flags UNUSED, + struct strbuf *err UNUSED) +{ + struct reftable_ref_store *refs = + reftable_be_downcast(ref_store, REF_STORE_WRITE, "init_db"); + struct strbuf sb = STRBUF_INIT; + + strbuf_addf(&sb, "%s/reftable", refs->base.gitdir); + safe_create_dir(sb.buf, 1); + strbuf_reset(&sb); + + strbuf_addf(&sb, "%s/HEAD", refs->base.gitdir); + write_file(sb.buf, "ref: refs/heads/.invalid"); + adjust_shared_perm(sb.buf); + strbuf_reset(&sb); + + strbuf_addf(&sb, "%s/refs", refs->base.gitdir); + safe_create_dir(sb.buf, 1); + strbuf_reset(&sb); + + strbuf_addf(&sb, "%s/refs/heads", refs->base.gitdir); + write_file(sb.buf, "this repository uses the reftable format"); + adjust_shared_perm(sb.buf); + + strbuf_release(&sb); + return 0; +} + +struct reftable_ref_iterator { + struct ref_iterator base; + struct reftable_ref_store *refs; + struct reftable_iterator iter; + struct reftable_ref_record ref; + struct object_id oid; + + const char *prefix; + unsigned int flags; + int err; +}; + +static int reftable_ref_iterator_advance(struct ref_iterator *ref_iterator) +{ + struct reftable_ref_iterator *iter = + (struct reftable_ref_iterator *)ref_iterator; + struct reftable_ref_store *refs = iter->refs; + + while (!iter->err) { + int flags = 0; + + iter->err = reftable_iterator_next_ref(&iter->iter, &iter->ref); + if (iter->err) + break; + + /* + * The files backend only lists references contained in + * "refs/". We emulate the same behaviour here and thus skip + * all references that don't start with this prefix. + */ + if (!starts_with(iter->ref.refname, "refs/")) + continue; + + if (iter->prefix && + strncmp(iter->prefix, iter->ref.refname, strlen(iter->prefix))) { + iter->err = 1; + break; + } + + if (iter->flags & DO_FOR_EACH_PER_WORKTREE_ONLY && + parse_worktree_ref(iter->ref.refname, NULL, NULL, NULL) != + REF_WORKTREE_CURRENT) + continue; + + switch (iter->ref.value_type) { + case REFTABLE_REF_VAL1: + oidread(&iter->oid, iter->ref.value.val1); + break; + case REFTABLE_REF_VAL2: + oidread(&iter->oid, iter->ref.value.val2.value); + break; + case REFTABLE_REF_SYMREF: + if (!refs_resolve_ref_unsafe(&iter->refs->base, iter->ref.refname, + RESOLVE_REF_READING, &iter->oid, &flags)) + oidclr(&iter->oid); + break; + default: + BUG("unhandled reference value type %d", iter->ref.value_type); + } + + if (is_null_oid(&iter->oid)) + flags |= REF_ISBROKEN; + + if (check_refname_format(iter->ref.refname, REFNAME_ALLOW_ONELEVEL)) { + if (!refname_is_safe(iter->ref.refname)) + die(_("refname is dangerous: %s"), iter->ref.refname); + oidclr(&iter->oid); + flags |= REF_BAD_NAME | REF_ISBROKEN; + } + + if (iter->flags & DO_FOR_EACH_OMIT_DANGLING_SYMREFS && + flags & REF_ISSYMREF && + flags & REF_ISBROKEN) + continue; + + if (!(iter->flags & DO_FOR_EACH_INCLUDE_BROKEN) && + !ref_resolves_to_object(iter->ref.refname, refs->base.repo, + &iter->oid, flags)) + continue; + + iter->base.refname = iter->ref.refname; + iter->base.oid = &iter->oid; + iter->base.flags = flags; + + break; + } + + if (iter->err > 0) { + if (ref_iterator_abort(ref_iterator) != ITER_DONE) + return ITER_ERROR; + return ITER_DONE; + } + + if (iter->err < 0) { + ref_iterator_abort(ref_iterator); + return ITER_ERROR; + } + + return ITER_OK; +} + +static int reftable_ref_iterator_peel(struct ref_iterator *ref_iterator, + struct object_id *peeled) +{ + struct reftable_ref_iterator *iter = + (struct reftable_ref_iterator *)ref_iterator; + + if (iter->ref.value_type == REFTABLE_REF_VAL2) { + oidread(peeled, iter->ref.value.val2.target_value); + return 0; + } + + return -1; +} + +static int reftable_ref_iterator_abort(struct ref_iterator *ref_iterator) +{ + struct reftable_ref_iterator *iter = + (struct reftable_ref_iterator *)ref_iterator; + reftable_ref_record_release(&iter->ref); + reftable_iterator_destroy(&iter->iter); + free(iter); + return ITER_DONE; +} + +static struct ref_iterator_vtable reftable_ref_iterator_vtable = { + .advance = reftable_ref_iterator_advance, + .peel = reftable_ref_iterator_peel, + .abort = reftable_ref_iterator_abort +}; + +static struct reftable_ref_iterator *ref_iterator_for_stack(struct reftable_ref_store *refs, + struct reftable_stack *stack, + const char *prefix, + int flags) +{ + struct reftable_merged_table *merged_table; + struct reftable_ref_iterator *iter; + int ret; + + iter = xcalloc(1, sizeof(*iter)); + base_ref_iterator_init(&iter->base, &reftable_ref_iterator_vtable, 1); + iter->prefix = prefix; + iter->base.oid = &iter->oid; + iter->flags = flags; + iter->refs = refs; + + ret = refs->err; + if (ret) + goto done; + + ret = reftable_stack_reload(stack); + if (ret) + goto done; + + merged_table = reftable_stack_merged_table(stack); + + ret = reftable_merged_table_seek_ref(merged_table, &iter->iter, prefix); + if (ret) + goto done; + +done: + iter->err = ret; + return iter; +} + +static enum iterator_selection iterator_select(struct ref_iterator *iter_worktree, + struct ref_iterator *iter_common, + void *cb_data UNUSED) +{ + if (iter_worktree && !iter_common) { + /* + * Return the worktree ref if there are no more common refs. + */ + return ITER_SELECT_0; + } else if (iter_common) { + /* + * In case we have pending worktree and common refs we need to + * yield them based on their lexicographical order. Worktree + * refs that have the same name as common refs shadow the + * latter. + */ + if (iter_worktree) { + int cmp = strcmp(iter_worktree->refname, + iter_common->refname); + if (cmp < 0) + return ITER_SELECT_0; + else if (!cmp) + return ITER_SELECT_0_SKIP_1; + } + + /* + * We now know that the lexicographically-next ref is a common + * ref. When the common ref is a shared one we return it. + */ + if (parse_worktree_ref(iter_common->refname, NULL, NULL, + NULL) == REF_WORKTREE_SHARED) + return ITER_SELECT_1; + + /* + * Otherwise, if the common ref is a per-worktree ref we skip + * it because it would belong to the main worktree, not ours. + */ + return ITER_SKIP_1; + } else { + return ITER_DONE; + } +} + +static struct ref_iterator *reftable_be_iterator_begin(struct ref_store *ref_store, + const char *prefix, + const char **exclude_patterns, + unsigned int flags) +{ + struct reftable_ref_iterator *main_iter, *worktree_iter; + struct reftable_ref_store *refs; + unsigned int required_flags = REF_STORE_READ; + + if (!(flags & DO_FOR_EACH_INCLUDE_BROKEN)) + required_flags |= REF_STORE_ODB; + refs = reftable_be_downcast(ref_store, required_flags, "ref_iterator_begin"); + + main_iter = ref_iterator_for_stack(refs, refs->main_stack, prefix, flags); + + /* + * The worktree stack is only set when we're in an actual worktree + * right now. If we aren't, then we return the common reftable + * iterator, only. + */ + if (!refs->worktree_stack) + return &main_iter->base; + + /* + * Otherwise we merge both the common and the per-worktree refs into a + * single iterator. + */ + worktree_iter = ref_iterator_for_stack(refs, refs->worktree_stack, prefix, flags); + return merge_ref_iterator_begin(1, &worktree_iter->base, &main_iter->base, + iterator_select, NULL); +} + +static int reftable_be_read_raw_ref(struct ref_store *ref_store, + const char *refname, + struct object_id *oid, + struct strbuf *referent, + unsigned int *type, + int *failure_errno) +{ + struct reftable_ref_store *refs = + reftable_be_downcast(ref_store, REF_STORE_READ, "read_raw_ref"); + struct reftable_stack *stack = stack_for(refs, refname, &refname); + int ret; + + if (refs->err < 0) + return refs->err; + + ret = reftable_stack_reload(stack); + if (ret) + return ret; + + ret = read_ref_without_reload(stack, refname, oid, referent, type); + if (ret < 0) + return ret; + if (ret > 0) { + *failure_errno = ENOENT; + return -1; + } + + return 0; +} + +static int reftable_be_read_symbolic_ref(struct ref_store *ref_store, + const char *refname, + struct strbuf *referent) +{ + struct reftable_ref_store *refs = + reftable_be_downcast(ref_store, REF_STORE_READ, "read_symbolic_ref"); + struct reftable_stack *stack = stack_for(refs, refname, &refname); + struct reftable_ref_record ref = {0}; + int ret; + + ret = reftable_stack_reload(stack); + if (ret) + return ret; + + ret = reftable_stack_read_ref(stack, refname, &ref); + if (ret == 0 && ref.value_type == REFTABLE_REF_SYMREF) + strbuf_addstr(referent, ref.value.symref); + else + ret = -1; + + reftable_ref_record_release(&ref); + return ret; +} + +/* + * Return the refname under which update was originally requested. + */ +static const char *original_update_refname(struct ref_update *update) +{ + while (update->parent_update) + update = update->parent_update; + return update->refname; +} + +struct reftable_transaction_update { + struct ref_update *update; + struct object_id current_oid; +}; + +struct write_transaction_table_arg { + struct reftable_ref_store *refs; + struct reftable_stack *stack; + struct reftable_addition *addition; + struct reftable_transaction_update *updates; + size_t updates_nr; + size_t updates_alloc; + size_t updates_expected; +}; + +struct reftable_transaction_data { + struct write_transaction_table_arg *args; + size_t args_nr, args_alloc; +}; + +static void free_transaction_data(struct reftable_transaction_data *tx_data) +{ + if (!tx_data) + return; + for (size_t i = 0; i < tx_data->args_nr; i++) { + reftable_addition_destroy(tx_data->args[i].addition); + free(tx_data->args[i].updates); + } + free(tx_data->args); + free(tx_data); +} + +/* + * Prepare transaction update for the given reference update. This will cause + * us to lock the corresponding reftable stack for concurrent modification. + */ +static int prepare_transaction_update(struct write_transaction_table_arg **out, + struct reftable_ref_store *refs, + struct reftable_transaction_data *tx_data, + struct ref_update *update, + struct strbuf *err) +{ + struct reftable_stack *stack = stack_for(refs, update->refname, NULL); + struct write_transaction_table_arg *arg = NULL; + size_t i; + int ret; + + /* + * Search for a preexisting stack update. If there is one then we add + * the update to it, otherwise we set up a new stack update. + */ + for (i = 0; !arg && i < tx_data->args_nr; i++) + if (tx_data->args[i].stack == stack) + arg = &tx_data->args[i]; + + if (!arg) { + struct reftable_addition *addition; + + ret = reftable_stack_reload(stack); + if (ret) + return ret; + + ret = reftable_stack_new_addition(&addition, stack); + if (ret) { + if (ret == REFTABLE_LOCK_ERROR) + strbuf_addstr(err, "cannot lock references"); + return ret; + } + + ALLOC_GROW(tx_data->args, tx_data->args_nr + 1, + tx_data->args_alloc); + arg = &tx_data->args[tx_data->args_nr++]; + arg->refs = refs; + arg->stack = stack; + arg->addition = addition; + arg->updates = NULL; + arg->updates_nr = 0; + arg->updates_alloc = 0; + arg->updates_expected = 0; + } + + arg->updates_expected++; + + if (out) + *out = arg; + + return 0; +} + +/* + * Queue a reference update for the correct stack. We potentially need to + * handle multiple stack updates in a single transaction when it spans across + * multiple worktrees. + */ +static int queue_transaction_update(struct reftable_ref_store *refs, + struct reftable_transaction_data *tx_data, + struct ref_update *update, + struct object_id *current_oid, + struct strbuf *err) +{ + struct write_transaction_table_arg *arg = NULL; + int ret; + + if (update->backend_data) + BUG("reference update queued more than once"); + + ret = prepare_transaction_update(&arg, refs, tx_data, update, err); + if (ret < 0) + return ret; + + ALLOC_GROW(arg->updates, arg->updates_nr + 1, + arg->updates_alloc); + arg->updates[arg->updates_nr].update = update; + oidcpy(&arg->updates[arg->updates_nr].current_oid, current_oid); + update->backend_data = &arg->updates[arg->updates_nr++]; + + return 0; +} + +static int reftable_be_transaction_prepare(struct ref_store *ref_store, + struct ref_transaction *transaction, + struct strbuf *err) +{ + struct reftable_ref_store *refs = + reftable_be_downcast(ref_store, REF_STORE_WRITE|REF_STORE_MAIN, "ref_transaction_prepare"); + struct strbuf referent = STRBUF_INIT, head_referent = STRBUF_INIT; + struct string_list affected_refnames = STRING_LIST_INIT_NODUP; + struct reftable_transaction_data *tx_data = NULL; + struct object_id head_oid; + unsigned int head_type = 0; + size_t i; + int ret; + + ret = refs->err; + if (ret < 0) + goto done; + + tx_data = xcalloc(1, sizeof(*tx_data)); + + /* + * Preprocess all updates. For one we check that there are no duplicate + * reference updates in this transaction. Second, we lock all stacks + * that will be modified during the transaction. + */ + for (i = 0; i < transaction->nr; i++) { + ret = prepare_transaction_update(NULL, refs, tx_data, + transaction->updates[i], err); + if (ret) + goto done; + + string_list_append(&affected_refnames, + transaction->updates[i]->refname); + } + + /* + * Now that we have counted updates per stack we can preallocate their + * arrays. This avoids having to reallocate many times. + */ + for (i = 0; i < tx_data->args_nr; i++) { + CALLOC_ARRAY(tx_data->args[i].updates, tx_data->args[i].updates_expected); + tx_data->args[i].updates_alloc = tx_data->args[i].updates_expected; + } + + /* + * Fail if a refname appears more than once in the transaction. + * This code is taken from the files backend and is a good candidate to + * be moved into the generic layer. + */ + string_list_sort(&affected_refnames); + if (ref_update_reject_duplicates(&affected_refnames, err)) { + ret = TRANSACTION_GENERIC_ERROR; + goto done; + } + + ret = read_ref_without_reload(stack_for(refs, "HEAD", NULL), "HEAD", &head_oid, + &head_referent, &head_type); + if (ret < 0) + goto done; + + for (i = 0; i < transaction->nr; i++) { + struct ref_update *u = transaction->updates[i]; + struct object_id current_oid = {0}; + struct reftable_stack *stack; + const char *rewritten_ref; + + stack = stack_for(refs, u->refname, &rewritten_ref); + + /* Verify that the new object ID is valid. */ + if ((u->flags & REF_HAVE_NEW) && !is_null_oid(&u->new_oid) && + !(u->flags & REF_SKIP_OID_VERIFICATION) && + !(u->flags & REF_LOG_ONLY)) { + struct object *o = parse_object(refs->base.repo, &u->new_oid); + if (!o) { + strbuf_addf(err, + _("trying to write ref '%s' with nonexistent object %s"), + u->refname, oid_to_hex(&u->new_oid)); + ret = -1; + goto done; + } + + if (o->type != OBJ_COMMIT && is_branch(u->refname)) { + strbuf_addf(err, _("trying to write non-commit object %s to branch '%s'"), + oid_to_hex(&u->new_oid), u->refname); + ret = -1; + goto done; + } + } + + /* + * When we update the reference that HEAD points to we enqueue + * a second log-only update for HEAD so that its reflog is + * updated accordingly. + */ + if (head_type == REF_ISSYMREF && + !(u->flags & REF_LOG_ONLY) && + !(u->flags & REF_UPDATE_VIA_HEAD) && + !strcmp(rewritten_ref, head_referent.buf)) { + struct ref_update *new_update; + + /* + * First make sure that HEAD is not already in the + * transaction. This check is O(lg N) in the transaction + * size, but it happens at most once per transaction. + */ + if (string_list_has_string(&affected_refnames, "HEAD")) { + /* An entry already existed */ + strbuf_addf(err, + _("multiple updates for 'HEAD' (including one " + "via its referent '%s') are not allowed"), + u->refname); + ret = TRANSACTION_NAME_CONFLICT; + goto done; + } + + new_update = ref_transaction_add_update( + transaction, "HEAD", + u->flags | REF_LOG_ONLY | REF_NO_DEREF, + &u->new_oid, &u->old_oid, u->msg); + string_list_insert(&affected_refnames, new_update->refname); + } + + ret = read_ref_without_reload(stack, rewritten_ref, + ¤t_oid, &referent, &u->type); + if (ret < 0) + goto done; + if (ret > 0 && (!(u->flags & REF_HAVE_OLD) || is_null_oid(&u->old_oid))) { + /* + * The reference does not exist, and we either have no + * old object ID or expect the reference to not exist. + * We can thus skip below safety checks as well as the + * symref splitting. But we do want to verify that + * there is no conflicting reference here so that we + * can output a proper error message instead of failing + * at a later point. + */ + ret = refs_verify_refname_available(ref_store, u->refname, + &affected_refnames, NULL, err); + if (ret < 0) + goto done; + + /* + * There is no need to write the reference deletion + * when the reference in question doesn't exist. + */ + if (u->flags & REF_HAVE_NEW && !is_null_oid(&u->new_oid)) { + ret = queue_transaction_update(refs, tx_data, u, + ¤t_oid, err); + if (ret) + goto done; + } + + continue; + } + if (ret > 0) { + /* The reference does not exist, but we expected it to. */ + strbuf_addf(err, _("cannot lock ref '%s': " + "unable to resolve reference '%s'"), + original_update_refname(u), u->refname); + ret = -1; + goto done; + } + + if (u->type & REF_ISSYMREF) { + /* + * The reftable stack is locked at this point already, + * so it is safe to call `refs_resolve_ref_unsafe()` + * here without causing races. + */ + const char *resolved = refs_resolve_ref_unsafe(&refs->base, u->refname, 0, + ¤t_oid, NULL); + + if (u->flags & REF_NO_DEREF) { + if (u->flags & REF_HAVE_OLD && !resolved) { + strbuf_addf(err, _("cannot lock ref '%s': " + "error reading reference"), u->refname); + ret = -1; + goto done; + } + } else { + struct ref_update *new_update; + int new_flags; + + new_flags = u->flags; + if (!strcmp(rewritten_ref, "HEAD")) + new_flags |= REF_UPDATE_VIA_HEAD; + + /* + * If we are updating a symref (eg. HEAD), we should also + * update the branch that the symref points to. + * + * This is generic functionality, and would be better + * done in refs.c, but the current implementation is + * intertwined with the locking in files-backend.c. + */ + new_update = ref_transaction_add_update( + transaction, referent.buf, new_flags, + &u->new_oid, &u->old_oid, u->msg); + new_update->parent_update = u; + + /* + * Change the symbolic ref update to log only. Also, it + * doesn't need to check its old OID value, as that will be + * done when new_update is processed. + */ + u->flags |= REF_LOG_ONLY | REF_NO_DEREF; + u->flags &= ~REF_HAVE_OLD; + + if (string_list_has_string(&affected_refnames, new_update->refname)) { + strbuf_addf(err, + _("multiple updates for '%s' (including one " + "via symref '%s') are not allowed"), + referent.buf, u->refname); + ret = TRANSACTION_NAME_CONFLICT; + goto done; + } + string_list_insert(&affected_refnames, new_update->refname); + } + } + + /* + * Verify that the old object matches our expectations. Note + * that the error messages here do not make a lot of sense in + * the context of the reftable backend as we never lock + * individual refs. But the error messages match what the files + * backend returns, which keeps our tests happy. + */ + if (u->flags & REF_HAVE_OLD && !oideq(¤t_oid, &u->old_oid)) { + if (is_null_oid(&u->old_oid)) + strbuf_addf(err, _("cannot lock ref '%s': " + "reference already exists"), + original_update_refname(u)); + else if (is_null_oid(¤t_oid)) + strbuf_addf(err, _("cannot lock ref '%s': " + "reference is missing but expected %s"), + original_update_refname(u), + oid_to_hex(&u->old_oid)); + else + strbuf_addf(err, _("cannot lock ref '%s': " + "is at %s but expected %s"), + original_update_refname(u), + oid_to_hex(¤t_oid), + oid_to_hex(&u->old_oid)); + ret = -1; + goto done; + } + + /* + * If all of the following conditions are true: + * + * - We're not about to write a symref. + * - We're not about to write a log-only entry. + * - Old and new object ID are different. + * + * Then we're essentially doing a no-op update that can be + * skipped. This is not only for the sake of efficiency, but + * also skips writing unneeded reflog entries. + */ + if ((u->type & REF_ISSYMREF) || + (u->flags & REF_LOG_ONLY) || + (u->flags & REF_HAVE_NEW && !oideq(¤t_oid, &u->new_oid))) { + ret = queue_transaction_update(refs, tx_data, u, + ¤t_oid, err); + if (ret) + goto done; + } + } + + transaction->backend_data = tx_data; + transaction->state = REF_TRANSACTION_PREPARED; + +done: + assert(ret != REFTABLE_API_ERROR); + if (ret < 0) { + free_transaction_data(tx_data); + transaction->state = REF_TRANSACTION_CLOSED; + if (!err->len) + strbuf_addf(err, _("reftable: transaction prepare: %s"), + reftable_error_str(ret)); + } + string_list_clear(&affected_refnames, 0); + strbuf_release(&referent); + strbuf_release(&head_referent); + + return ret; +} + +static int reftable_be_transaction_abort(struct ref_store *ref_store, + struct ref_transaction *transaction, + struct strbuf *err) +{ + struct reftable_transaction_data *tx_data = transaction->backend_data; + free_transaction_data(tx_data); + transaction->state = REF_TRANSACTION_CLOSED; + return 0; +} + +static int transaction_update_cmp(const void *a, const void *b) +{ + return strcmp(((struct reftable_transaction_update *)a)->update->refname, + ((struct reftable_transaction_update *)b)->update->refname); +} + +static int write_transaction_table(struct reftable_writer *writer, void *cb_data) +{ + struct write_transaction_table_arg *arg = cb_data; + struct reftable_merged_table *mt = + reftable_stack_merged_table(arg->stack); + uint64_t ts = reftable_stack_next_update_index(arg->stack); + struct reftable_log_record *logs = NULL; + size_t logs_nr = 0, logs_alloc = 0, i; + int ret = 0; + + QSORT(arg->updates, arg->updates_nr, transaction_update_cmp); + + reftable_writer_set_limits(writer, ts, ts); + + for (i = 0; i < arg->updates_nr; i++) { + struct reftable_transaction_update *tx_update = &arg->updates[i]; + struct ref_update *u = tx_update->update; + + /* + * Write a reflog entry when updating a ref to point to + * something new in either of the following cases: + * + * - The reference is about to be deleted. We always want to + * delete the reflog in that case. + * - REF_FORCE_CREATE_REFLOG is set, asking us to always create + * the reflog entry. + * - `core.logAllRefUpdates` tells us to create the reflog for + * the given ref. + */ + if (u->flags & REF_HAVE_NEW && !(u->type & REF_ISSYMREF) && is_null_oid(&u->new_oid)) { + struct reftable_log_record log = {0}; + struct reftable_iterator it = {0}; + + /* + * When deleting refs we also delete all reflog entries + * with them. While it is not strictly required to + * delete reflogs together with their refs, this + * matches the behaviour of the files backend. + * + * Unfortunately, we have no better way than to delete + * all reflog entries one by one. + */ + ret = reftable_merged_table_seek_log(mt, &it, u->refname); + while (ret == 0) { + struct reftable_log_record *tombstone; + + ret = reftable_iterator_next_log(&it, &log); + if (ret < 0) + break; + if (ret > 0 || strcmp(log.refname, u->refname)) { + ret = 0; + break; + } + + ALLOC_GROW(logs, logs_nr + 1, logs_alloc); + tombstone = &logs[logs_nr++]; + tombstone->refname = xstrdup(u->refname); + tombstone->value_type = REFTABLE_LOG_DELETION; + tombstone->update_index = log.update_index; + } + + reftable_log_record_release(&log); + reftable_iterator_destroy(&it); + + if (ret) + goto done; + } else if (u->flags & REF_HAVE_NEW && + (u->flags & REF_FORCE_CREATE_REFLOG || + should_write_log(&arg->refs->base, u->refname))) { + struct reftable_log_record *log; + + ALLOC_GROW(logs, logs_nr + 1, logs_alloc); + log = &logs[logs_nr++]; + memset(log, 0, sizeof(*log)); + + fill_reftable_log_record(log); + log->update_index = ts; + log->refname = xstrdup(u->refname); + log->value.update.new_hash = u->new_oid.hash; + log->value.update.old_hash = tx_update->current_oid.hash; + log->value.update.message = + xstrndup(u->msg, arg->refs->write_options.block_size / 2); + } + + if (u->flags & REF_LOG_ONLY) + continue; + + if (u->flags & REF_HAVE_NEW && is_null_oid(&u->new_oid)) { + struct reftable_ref_record ref = { + .refname = (char *)u->refname, + .update_index = ts, + .value_type = REFTABLE_REF_DELETION, + }; + + ret = reftable_writer_add_ref(writer, &ref); + if (ret < 0) + goto done; + } else if (u->flags & REF_HAVE_NEW) { + struct reftable_ref_record ref = {0}; + struct object_id peeled; + int peel_error; + + ref.refname = (char *)u->refname; + ref.update_index = ts; + + peel_error = peel_object(&u->new_oid, &peeled); + if (!peel_error) { + ref.value_type = REFTABLE_REF_VAL2; + memcpy(ref.value.val2.target_value, peeled.hash, GIT_MAX_RAWSZ); + memcpy(ref.value.val2.value, u->new_oid.hash, GIT_MAX_RAWSZ); + } else if (!is_null_oid(&u->new_oid)) { + ref.value_type = REFTABLE_REF_VAL1; + memcpy(ref.value.val1, u->new_oid.hash, GIT_MAX_RAWSZ); + } + + ret = reftable_writer_add_ref(writer, &ref); + if (ret < 0) + goto done; + } + } + + /* + * Logs are written at the end so that we do not have intermixed ref + * and log blocks. + */ + if (logs) { + ret = reftable_writer_add_logs(writer, logs, logs_nr); + if (ret < 0) + goto done; + } + +done: + assert(ret != REFTABLE_API_ERROR); + for (i = 0; i < logs_nr; i++) + clear_reftable_log_record(&logs[i]); + free(logs); + return ret; +} + +static int reftable_be_transaction_finish(struct ref_store *ref_store, + struct ref_transaction *transaction, + struct strbuf *err) +{ + struct reftable_transaction_data *tx_data = transaction->backend_data; + int ret = 0; + + for (size_t i = 0; i < tx_data->args_nr; i++) { + ret = reftable_addition_add(tx_data->args[i].addition, + write_transaction_table, &tx_data->args[i]); + if (ret < 0) + goto done; + + ret = reftable_addition_commit(tx_data->args[i].addition); + if (ret < 0) + goto done; + } + +done: + assert(ret != REFTABLE_API_ERROR); + free_transaction_data(tx_data); + transaction->state = REF_TRANSACTION_CLOSED; + + if (ret) { + strbuf_addf(err, _("reftable: transaction failure: %s"), + reftable_error_str(ret)); + return -1; + } + return ret; +} + +static int reftable_be_initial_transaction_commit(struct ref_store *ref_store UNUSED, + struct ref_transaction *transaction, + struct strbuf *err) +{ + return ref_transaction_commit(transaction, err); +} + +static int reftable_be_pack_refs(struct ref_store *ref_store, + struct pack_refs_opts *opts) +{ + struct reftable_ref_store *refs = + reftable_be_downcast(ref_store, REF_STORE_WRITE | REF_STORE_ODB, "pack_refs"); + struct reftable_stack *stack; + int ret; + + if (refs->err) + return refs->err; + + stack = refs->worktree_stack; + if (!stack) + stack = refs->main_stack; + + ret = reftable_stack_compact_all(stack, NULL); + if (ret) + goto out; + ret = reftable_stack_clean(stack); + if (ret) + goto out; + +out: + return ret; +} + +struct write_create_symref_arg { + struct reftable_ref_store *refs; + struct reftable_stack *stack; + const char *refname; + const char *target; + const char *logmsg; +}; + +static int write_create_symref_table(struct reftable_writer *writer, void *cb_data) +{ + struct write_create_symref_arg *create = cb_data; + uint64_t ts = reftable_stack_next_update_index(create->stack); + struct reftable_ref_record ref = { + .refname = (char *)create->refname, + .value_type = REFTABLE_REF_SYMREF, + .value.symref = (char *)create->target, + .update_index = ts, + }; + struct reftable_log_record log = {0}; + struct object_id new_oid; + struct object_id old_oid; + int ret; + + reftable_writer_set_limits(writer, ts, ts); + + ret = reftable_writer_add_ref(writer, &ref); + if (ret) + return ret; + + /* + * Note that it is important to try and resolve the reference before we + * write the log entry. This is because `should_write_log()` will munge + * `core.logAllRefUpdates`, which is undesirable when we create a new + * repository because it would be written into the config. As HEAD will + * not resolve for new repositories this ordering will ensure that this + * never happens. + */ + if (!create->logmsg || + !refs_resolve_ref_unsafe(&create->refs->base, create->target, + RESOLVE_REF_READING, &new_oid, NULL) || + !should_write_log(&create->refs->base, create->refname)) + return 0; + + fill_reftable_log_record(&log); + log.refname = xstrdup(create->refname); + log.update_index = ts; + log.value.update.message = xstrndup(create->logmsg, + create->refs->write_options.block_size / 2); + log.value.update.new_hash = new_oid.hash; + if (refs_resolve_ref_unsafe(&create->refs->base, create->refname, + RESOLVE_REF_READING, &old_oid, NULL)) + log.value.update.old_hash = old_oid.hash; + + ret = reftable_writer_add_log(writer, &log); + clear_reftable_log_record(&log); + return ret; +} + +static int reftable_be_create_symref(struct ref_store *ref_store, + const char *refname, + const char *target, + const char *logmsg) +{ + struct reftable_ref_store *refs = + reftable_be_downcast(ref_store, REF_STORE_WRITE, "create_symref"); + struct reftable_stack *stack = stack_for(refs, refname, &refname); + struct write_create_symref_arg arg = { + .refs = refs, + .stack = stack, + .refname = refname, + .target = target, + .logmsg = logmsg, + }; + int ret; + + ret = refs->err; + if (ret < 0) + goto done; + + ret = reftable_stack_reload(stack); + if (ret) + goto done; + + ret = reftable_stack_add(stack, &write_create_symref_table, &arg); + +done: + assert(ret != REFTABLE_API_ERROR); + if (ret) + error("unable to write symref for %s: %s", refname, + reftable_error_str(ret)); + return ret; +} + +struct write_copy_arg { + struct reftable_ref_store *refs; + struct reftable_stack *stack; + const char *oldname; + const char *newname; + const char *logmsg; + int delete_old; +}; + +static int write_copy_table(struct reftable_writer *writer, void *cb_data) +{ + struct write_copy_arg *arg = cb_data; + uint64_t deletion_ts, creation_ts; + struct reftable_merged_table *mt = reftable_stack_merged_table(arg->stack); + struct reftable_ref_record old_ref = {0}, refs[2] = {0}; + struct reftable_log_record old_log = {0}, *logs = NULL; + struct reftable_iterator it = {0}; + struct string_list skip = STRING_LIST_INIT_NODUP; + struct strbuf errbuf = STRBUF_INIT; + size_t logs_nr = 0, logs_alloc = 0, i; + int ret; + + if (reftable_stack_read_ref(arg->stack, arg->oldname, &old_ref)) { + ret = error(_("refname %s not found"), arg->oldname); + goto done; + } + if (old_ref.value_type == REFTABLE_REF_SYMREF) { + ret = error(_("refname %s is a symbolic ref, copying it is not supported"), + arg->oldname); + goto done; + } + + /* + * There's nothing to do in case the old and new name are the same, so + * we exit early in that case. + */ + if (!strcmp(arg->oldname, arg->newname)) { + ret = 0; + goto done; + } + + /* + * Verify that the new refname is available. + */ + string_list_insert(&skip, arg->oldname); + ret = refs_verify_refname_available(&arg->refs->base, arg->newname, + NULL, &skip, &errbuf); + if (ret < 0) { + error("%s", errbuf.buf); + goto done; + } + + /* + * When deleting the old reference we have to use two update indices: + * once to delete the old ref and its reflog, and once to create the + * new ref and its reflog. They need to be staged with two separate + * indices because the new reflog needs to encode both the deletion of + * the old branch and the creation of the new branch, and we cannot do + * two changes to a reflog in a single update. + */ + deletion_ts = creation_ts = reftable_stack_next_update_index(arg->stack); + if (arg->delete_old) + creation_ts++; + reftable_writer_set_limits(writer, deletion_ts, creation_ts); + + /* + * Add the new reference. If this is a rename then we also delete the + * old reference. + */ + refs[0] = old_ref; + refs[0].refname = (char *)arg->newname; + refs[0].update_index = creation_ts; + if (arg->delete_old) { + refs[1].refname = (char *)arg->oldname; + refs[1].value_type = REFTABLE_REF_DELETION; + refs[1].update_index = deletion_ts; + } + ret = reftable_writer_add_refs(writer, refs, arg->delete_old ? 2 : 1); + if (ret < 0) + goto done; + + /* + * When deleting the old branch we need to create a reflog entry on the + * new branch name that indicates that the old branch has been deleted + * and then recreated. This is a tad weird, but matches what the files + * backend does. + */ + if (arg->delete_old) { + struct strbuf head_referent = STRBUF_INIT; + struct object_id head_oid; + int append_head_reflog; + unsigned head_type = 0; + + ALLOC_GROW(logs, logs_nr + 1, logs_alloc); + memset(&logs[logs_nr], 0, sizeof(logs[logs_nr])); + fill_reftable_log_record(&logs[logs_nr]); + logs[logs_nr].refname = (char *)arg->newname; + logs[logs_nr].update_index = deletion_ts; + logs[logs_nr].value.update.message = + xstrndup(arg->logmsg, arg->refs->write_options.block_size / 2); + logs[logs_nr].value.update.old_hash = old_ref.value.val1; + logs_nr++; + + ret = read_ref_without_reload(arg->stack, "HEAD", &head_oid, &head_referent, &head_type); + if (ret < 0) + goto done; + append_head_reflog = (head_type & REF_ISSYMREF) && !strcmp(head_referent.buf, arg->oldname); + strbuf_release(&head_referent); + + /* + * The files backend uses `refs_delete_ref()` to delete the old + * branch name, which will append a reflog entry for HEAD in + * case it points to the old branch. + */ + if (append_head_reflog) { + ALLOC_GROW(logs, logs_nr + 1, logs_alloc); + logs[logs_nr] = logs[logs_nr - 1]; + logs[logs_nr].refname = "HEAD"; + logs_nr++; + } + } + + /* + * Create the reflog entry for the newly created branch. + */ + ALLOC_GROW(logs, logs_nr + 1, logs_alloc); + memset(&logs[logs_nr], 0, sizeof(logs[logs_nr])); + fill_reftable_log_record(&logs[logs_nr]); + logs[logs_nr].refname = (char *)arg->newname; + logs[logs_nr].update_index = creation_ts; + logs[logs_nr].value.update.message = + xstrndup(arg->logmsg, arg->refs->write_options.block_size / 2); + logs[logs_nr].value.update.new_hash = old_ref.value.val1; + logs_nr++; + + /* + * In addition to writing the reflog entry for the new branch, we also + * copy over all log entries from the old reflog. Last but not least, + * when renaming we also have to delete all the old reflog entries. + */ + ret = reftable_merged_table_seek_log(mt, &it, arg->oldname); + if (ret < 0) + goto done; + + while (1) { + ret = reftable_iterator_next_log(&it, &old_log); + if (ret < 0) + goto done; + if (ret > 0 || strcmp(old_log.refname, arg->oldname)) { + ret = 0; + break; + } + + free(old_log.refname); + + /* + * Copy over the old reflog entry with the new refname. + */ + ALLOC_GROW(logs, logs_nr + 1, logs_alloc); + logs[logs_nr] = old_log; + logs[logs_nr].refname = (char *)arg->newname; + logs_nr++; + + /* + * Delete the old reflog entry in case we are renaming. + */ + if (arg->delete_old) { + ALLOC_GROW(logs, logs_nr + 1, logs_alloc); + memset(&logs[logs_nr], 0, sizeof(logs[logs_nr])); + logs[logs_nr].refname = (char *)arg->oldname; + logs[logs_nr].value_type = REFTABLE_LOG_DELETION; + logs[logs_nr].update_index = old_log.update_index; + logs_nr++; + } + + /* + * Transfer ownership of the log record we're iterating over to + * the array of log records. Otherwise, the pointers would get + * free'd or reallocated by the iterator. + */ + memset(&old_log, 0, sizeof(old_log)); + } + + ret = reftable_writer_add_logs(writer, logs, logs_nr); + if (ret < 0) + goto done; + +done: + assert(ret != REFTABLE_API_ERROR); + reftable_iterator_destroy(&it); + string_list_clear(&skip, 0); + strbuf_release(&errbuf); + for (i = 0; i < logs_nr; i++) { + if (!strcmp(logs[i].refname, "HEAD")) + continue; + if (logs[i].value.update.old_hash == old_ref.value.val1) + logs[i].value.update.old_hash = NULL; + if (logs[i].value.update.new_hash == old_ref.value.val1) + logs[i].value.update.new_hash = NULL; + logs[i].refname = NULL; + reftable_log_record_release(&logs[i]); + } + free(logs); + reftable_ref_record_release(&old_ref); + reftable_log_record_release(&old_log); + return ret; +} + +static int reftable_be_rename_ref(struct ref_store *ref_store, + const char *oldrefname, + const char *newrefname, + const char *logmsg) +{ + struct reftable_ref_store *refs = + reftable_be_downcast(ref_store, REF_STORE_WRITE, "rename_ref"); + struct reftable_stack *stack = stack_for(refs, newrefname, &newrefname); + struct write_copy_arg arg = { + .refs = refs, + .stack = stack, + .oldname = oldrefname, + .newname = newrefname, + .logmsg = logmsg, + .delete_old = 1, + }; + int ret; + + ret = refs->err; + if (ret < 0) + goto done; + + ret = reftable_stack_reload(stack); + if (ret) + goto done; + ret = reftable_stack_add(stack, &write_copy_table, &arg); + +done: + assert(ret != REFTABLE_API_ERROR); + return ret; +} + +static int reftable_be_copy_ref(struct ref_store *ref_store, + const char *oldrefname, + const char *newrefname, + const char *logmsg) +{ + struct reftable_ref_store *refs = + reftable_be_downcast(ref_store, REF_STORE_WRITE, "copy_ref"); + struct reftable_stack *stack = stack_for(refs, newrefname, &newrefname); + struct write_copy_arg arg = { + .refs = refs, + .stack = stack, + .oldname = oldrefname, + .newname = newrefname, + .logmsg = logmsg, + }; + int ret; + + ret = refs->err; + if (ret < 0) + goto done; + + ret = reftable_stack_reload(stack); + if (ret) + goto done; + ret = reftable_stack_add(stack, &write_copy_table, &arg); + +done: + assert(ret != REFTABLE_API_ERROR); + return ret; +} + +struct reftable_reflog_iterator { + struct ref_iterator base; + struct reftable_ref_store *refs; + struct reftable_iterator iter; + struct reftable_log_record log; + struct object_id oid; + char *last_name; + int err; +}; + +static int reftable_reflog_iterator_advance(struct ref_iterator *ref_iterator) +{ + struct reftable_reflog_iterator *iter = + (struct reftable_reflog_iterator *)ref_iterator; + + while (!iter->err) { + int flags; + + iter->err = reftable_iterator_next_log(&iter->iter, &iter->log); + if (iter->err) + break; + + /* + * We want the refnames that we have reflogs for, so we skip if + * we've already produced this name. This could be faster by + * seeking directly to reflog@update_index==0. + */ + if (iter->last_name && !strcmp(iter->log.refname, iter->last_name)) + continue; + + if (!refs_resolve_ref_unsafe(&iter->refs->base, iter->log.refname, + 0, &iter->oid, &flags)) { + error(_("bad ref for %s"), iter->log.refname); + continue; + } + + free(iter->last_name); + iter->last_name = xstrdup(iter->log.refname); + iter->base.refname = iter->log.refname; + iter->base.oid = &iter->oid; + iter->base.flags = flags; + + break; + } + + if (iter->err > 0) { + if (ref_iterator_abort(ref_iterator) != ITER_DONE) + return ITER_ERROR; + return ITER_DONE; + } + + if (iter->err < 0) { + ref_iterator_abort(ref_iterator); + return ITER_ERROR; + } + + return ITER_OK; +} + +static int reftable_reflog_iterator_peel(struct ref_iterator *ref_iterator, + struct object_id *peeled) +{ + BUG("reftable reflog iterator cannot be peeled"); + return -1; +} + +static int reftable_reflog_iterator_abort(struct ref_iterator *ref_iterator) +{ + struct reftable_reflog_iterator *iter = + (struct reftable_reflog_iterator *)ref_iterator; + reftable_log_record_release(&iter->log); + reftable_iterator_destroy(&iter->iter); + free(iter->last_name); + free(iter); + return ITER_DONE; +} + +static struct ref_iterator_vtable reftable_reflog_iterator_vtable = { + .advance = reftable_reflog_iterator_advance, + .peel = reftable_reflog_iterator_peel, + .abort = reftable_reflog_iterator_abort +}; + +static struct reftable_reflog_iterator *reflog_iterator_for_stack(struct reftable_ref_store *refs, + struct reftable_stack *stack) +{ + struct reftable_merged_table *merged_table; + struct reftable_reflog_iterator *iter; + int ret; + + iter = xcalloc(1, sizeof(*iter)); + base_ref_iterator_init(&iter->base, &reftable_reflog_iterator_vtable, 1); + iter->refs = refs; + iter->base.oid = &iter->oid; + + ret = refs->err; + if (ret) + goto done; + + ret = reftable_stack_reload(refs->main_stack); + if (ret < 0) + goto done; + + merged_table = reftable_stack_merged_table(stack); + + ret = reftable_merged_table_seek_log(merged_table, &iter->iter, ""); + if (ret < 0) + goto done; + +done: + iter->err = ret; + return iter; +} + +static struct ref_iterator *reftable_be_reflog_iterator_begin(struct ref_store *ref_store) +{ + struct reftable_ref_store *refs = + reftable_be_downcast(ref_store, REF_STORE_READ, "reflog_iterator_begin"); + struct reftable_reflog_iterator *main_iter, *worktree_iter; + + main_iter = reflog_iterator_for_stack(refs, refs->main_stack); + if (!refs->worktree_stack) + return &main_iter->base; + + worktree_iter = reflog_iterator_for_stack(refs, refs->worktree_stack); + + return merge_ref_iterator_begin(1, &worktree_iter->base, &main_iter->base, + iterator_select, NULL); +} + +static int yield_log_record(struct reftable_log_record *log, + each_reflog_ent_fn fn, + void *cb_data) +{ + struct object_id old_oid, new_oid; + const char *full_committer; + + oidread(&old_oid, log->value.update.old_hash); + oidread(&new_oid, log->value.update.new_hash); + + /* + * When both the old object ID and the new object ID are null + * then this is the reflog existence marker. The caller must + * not be aware of it. + */ + if (is_null_oid(&old_oid) && is_null_oid(&new_oid)) + return 0; + + full_committer = fmt_ident(log->value.update.name, log->value.update.email, + WANT_COMMITTER_IDENT, NULL, IDENT_NO_DATE); + return fn(&old_oid, &new_oid, full_committer, + log->value.update.time, log->value.update.tz_offset, + log->value.update.message, cb_data); +} + +static int reftable_be_for_each_reflog_ent_reverse(struct ref_store *ref_store, + const char *refname, + each_reflog_ent_fn fn, + void *cb_data) +{ + struct reftable_ref_store *refs = + reftable_be_downcast(ref_store, REF_STORE_READ, "for_each_reflog_ent_reverse"); + struct reftable_stack *stack = stack_for(refs, refname, &refname); + struct reftable_merged_table *mt = NULL; + struct reftable_log_record log = {0}; + struct reftable_iterator it = {0}; + int ret; + + if (refs->err < 0) + return refs->err; + + mt = reftable_stack_merged_table(stack); + ret = reftable_merged_table_seek_log(mt, &it, refname); + while (!ret) { + ret = reftable_iterator_next_log(&it, &log); + if (ret < 0) + break; + if (ret > 0 || strcmp(log.refname, refname)) { + ret = 0; + break; + } + + ret = yield_log_record(&log, fn, cb_data); + if (ret) + break; + } + + reftable_log_record_release(&log); + reftable_iterator_destroy(&it); + return ret; +} + +static int reftable_be_for_each_reflog_ent(struct ref_store *ref_store, + const char *refname, + each_reflog_ent_fn fn, + void *cb_data) +{ + struct reftable_ref_store *refs = + reftable_be_downcast(ref_store, REF_STORE_READ, "for_each_reflog_ent"); + struct reftable_stack *stack = stack_for(refs, refname, &refname); + struct reftable_merged_table *mt = NULL; + struct reftable_log_record *logs = NULL; + struct reftable_iterator it = {0}; + size_t logs_alloc = 0, logs_nr = 0, i; + int ret; + + if (refs->err < 0) + return refs->err; + + mt = reftable_stack_merged_table(stack); + ret = reftable_merged_table_seek_log(mt, &it, refname); + while (!ret) { + struct reftable_log_record log = {0}; + + ret = reftable_iterator_next_log(&it, &log); + if (ret < 0) + goto done; + if (ret > 0 || strcmp(log.refname, refname)) { + reftable_log_record_release(&log); + ret = 0; + break; + } + + ALLOC_GROW(logs, logs_nr + 1, logs_alloc); + logs[logs_nr++] = log; + } + + for (i = logs_nr; i--;) { + ret = yield_log_record(&logs[i], fn, cb_data); + if (ret) + goto done; + } + +done: + reftable_iterator_destroy(&it); + for (i = 0; i < logs_nr; i++) + reftable_log_record_release(&logs[i]); + free(logs); + return ret; +} + +static int reftable_be_reflog_exists(struct ref_store *ref_store, + const char *refname) +{ + struct reftable_ref_store *refs = + reftable_be_downcast(ref_store, REF_STORE_READ, "reflog_exists"); + struct reftable_stack *stack = stack_for(refs, refname, &refname); + struct reftable_merged_table *mt = reftable_stack_merged_table(stack); + struct reftable_log_record log = {0}; + struct reftable_iterator it = {0}; + int ret; + + ret = refs->err; + if (ret < 0) + goto done; + + ret = reftable_stack_reload(stack); + if (ret < 0) + goto done; + + ret = reftable_merged_table_seek_log(mt, &it, refname); + if (ret < 0) + goto done; + + /* + * Check whether we get at least one log record for the given ref name. + * If so, the reflog exists, otherwise it doesn't. + */ + ret = reftable_iterator_next_log(&it, &log); + if (ret < 0) + goto done; + if (ret > 0) { + ret = 0; + goto done; + } + + ret = strcmp(log.refname, refname) == 0; + +done: + reftable_iterator_destroy(&it); + reftable_log_record_release(&log); + if (ret < 0) + ret = 0; + return ret; +} + +struct write_reflog_existence_arg { + struct reftable_ref_store *refs; + const char *refname; + struct reftable_stack *stack; +}; + +static int write_reflog_existence_table(struct reftable_writer *writer, + void *cb_data) +{ + struct write_reflog_existence_arg *arg = cb_data; + uint64_t ts = reftable_stack_next_update_index(arg->stack); + struct reftable_log_record log = {0}; + int ret; + + ret = reftable_stack_read_log(arg->stack, arg->refname, &log); + if (ret <= 0) + goto done; + + reftable_writer_set_limits(writer, ts, ts); + + /* + * The existence entry has both old and new object ID set to the the + * null object ID. Our iterators are aware of this and will not present + * them to their callers. + */ + log.refname = xstrdup(arg->refname); + log.update_index = ts; + log.value_type = REFTABLE_LOG_UPDATE; + ret = reftable_writer_add_log(writer, &log); + +done: + assert(ret != REFTABLE_API_ERROR); + reftable_log_record_release(&log); + return ret; +} + +static int reftable_be_create_reflog(struct ref_store *ref_store, + const char *refname, + struct strbuf *errmsg) +{ + struct reftable_ref_store *refs = + reftable_be_downcast(ref_store, REF_STORE_WRITE, "create_reflog"); + struct reftable_stack *stack = stack_for(refs, refname, &refname); + struct write_reflog_existence_arg arg = { + .refs = refs, + .stack = stack, + .refname = refname, + }; + int ret; + + ret = refs->err; + if (ret < 0) + goto done; + + ret = reftable_stack_reload(stack); + if (ret) + goto done; + + ret = reftable_stack_add(stack, &write_reflog_existence_table, &arg); + +done: + return ret; +} + +struct write_reflog_delete_arg { + struct reftable_stack *stack; + const char *refname; +}; + +static int write_reflog_delete_table(struct reftable_writer *writer, void *cb_data) +{ + struct write_reflog_delete_arg *arg = cb_data; + struct reftable_merged_table *mt = + reftable_stack_merged_table(arg->stack); + struct reftable_log_record log = {0}, tombstone = {0}; + struct reftable_iterator it = {0}; + uint64_t ts = reftable_stack_next_update_index(arg->stack); + int ret; + + reftable_writer_set_limits(writer, ts, ts); + + /* + * In order to delete a table we need to delete all reflog entries one + * by one. This is inefficient, but the reftable format does not have a + * better marker right now. + */ + ret = reftable_merged_table_seek_log(mt, &it, arg->refname); + while (ret == 0) { + ret = reftable_iterator_next_log(&it, &log); + if (ret < 0) + break; + if (ret > 0 || strcmp(log.refname, arg->refname)) { + ret = 0; + break; + } + + tombstone.refname = (char *)arg->refname; + tombstone.value_type = REFTABLE_LOG_DELETION; + tombstone.update_index = log.update_index; + + ret = reftable_writer_add_log(writer, &tombstone); + } + + reftable_log_record_release(&log); + reftable_iterator_destroy(&it); + return ret; +} + +static int reftable_be_delete_reflog(struct ref_store *ref_store, + const char *refname) +{ + struct reftable_ref_store *refs = + reftable_be_downcast(ref_store, REF_STORE_WRITE, "delete_reflog"); + struct reftable_stack *stack = stack_for(refs, refname, &refname); + struct write_reflog_delete_arg arg = { + .stack = stack, + .refname = refname, + }; + int ret; + + ret = reftable_stack_reload(stack); + if (ret) + return ret; + ret = reftable_stack_add(stack, &write_reflog_delete_table, &arg); + + assert(ret != REFTABLE_API_ERROR); + return ret; +} + +struct reflog_expiry_arg { + struct reftable_stack *stack; + struct reftable_log_record *records; + struct object_id update_oid; + const char *refname; + size_t len; +}; + +static int write_reflog_expiry_table(struct reftable_writer *writer, void *cb_data) +{ + struct reflog_expiry_arg *arg = cb_data; + uint64_t ts = reftable_stack_next_update_index(arg->stack); + uint64_t live_records = 0; + size_t i; + int ret; + + for (i = 0; i < arg->len; i++) + if (arg->records[i].value_type == REFTABLE_LOG_UPDATE) + live_records++; + + reftable_writer_set_limits(writer, ts, ts); + + if (!is_null_oid(&arg->update_oid)) { + struct reftable_ref_record ref = {0}; + struct object_id peeled; + + ref.refname = (char *)arg->refname; + ref.update_index = ts; + + if (!peel_object(&arg->update_oid, &peeled)) { + ref.value_type = REFTABLE_REF_VAL2; + memcpy(ref.value.val2.target_value, peeled.hash, GIT_MAX_RAWSZ); + memcpy(ref.value.val2.value, arg->update_oid.hash, GIT_MAX_RAWSZ); + } else { + ref.value_type = REFTABLE_REF_VAL1; + memcpy(ref.value.val1, arg->update_oid.hash, GIT_MAX_RAWSZ); + } + + ret = reftable_writer_add_ref(writer, &ref); + if (ret < 0) + return ret; + } + + /* + * When there are no more entries left in the reflog we empty it + * completely, but write a placeholder reflog entry that indicates that + * the reflog still exists. + */ + if (!live_records) { + struct reftable_log_record log = { + .refname = (char *)arg->refname, + .value_type = REFTABLE_LOG_UPDATE, + .update_index = ts, + }; + + ret = reftable_writer_add_log(writer, &log); + if (ret) + return ret; + } + + for (i = 0; i < arg->len; i++) { + ret = reftable_writer_add_log(writer, &arg->records[i]); + if (ret) + return ret; + } + + return 0; +} + +static int reftable_be_reflog_expire(struct ref_store *ref_store, + const char *refname, + unsigned int flags, + reflog_expiry_prepare_fn prepare_fn, + reflog_expiry_should_prune_fn should_prune_fn, + reflog_expiry_cleanup_fn cleanup_fn, + void *policy_cb_data) +{ + /* + * For log expiry, we write tombstones for every single reflog entry + * that is to be expired. This means that the entries are still + * retrievable by delving into the stack, and expiring entries + * paradoxically takes extra memory. This memory is only reclaimed when + * compacting the reftable stack. + * + * It would be better if the refs backend supported an API that sets a + * criterion for all refs, passing the criterion to pack_refs(). + * + * On the plus side, because we do the expiration per ref, we can easily + * insert the reflog existence dummies. + */ + struct reftable_ref_store *refs = + reftable_be_downcast(ref_store, REF_STORE_WRITE, "reflog_expire"); + struct reftable_stack *stack = stack_for(refs, refname, &refname); + struct reftable_merged_table *mt = reftable_stack_merged_table(stack); + struct reftable_log_record *logs = NULL; + struct reftable_log_record *rewritten = NULL; + struct reftable_ref_record ref_record = {0}; + struct reftable_iterator it = {0}; + struct reftable_addition *add = NULL; + struct reflog_expiry_arg arg = {0}; + struct object_id oid = {0}; + uint8_t *last_hash = NULL; + size_t logs_nr = 0, logs_alloc = 0, i; + int ret; + + if (refs->err < 0) + return refs->err; + + ret = reftable_stack_reload(stack); + if (ret < 0) + goto done; + + ret = reftable_merged_table_seek_log(mt, &it, refname); + if (ret < 0) + goto done; + + ret = reftable_stack_new_addition(&add, stack); + if (ret < 0) + goto done; + + ret = reftable_stack_read_ref(stack, refname, &ref_record); + if (ret < 0) + goto done; + if (reftable_ref_record_val1(&ref_record)) + oidread(&oid, reftable_ref_record_val1(&ref_record)); + prepare_fn(refname, &oid, policy_cb_data); + + while (1) { + struct reftable_log_record log = {0}; + struct object_id old_oid, new_oid; + + ret = reftable_iterator_next_log(&it, &log); + if (ret < 0) + goto done; + if (ret > 0 || strcmp(log.refname, refname)) { + reftable_log_record_release(&log); + break; + } + + oidread(&old_oid, log.value.update.old_hash); + oidread(&new_oid, log.value.update.new_hash); + + /* + * Skip over the reflog existence marker. We will add it back + * in when there are no live reflog records. + */ + if (is_null_oid(&old_oid) && is_null_oid(&new_oid)) { + reftable_log_record_release(&log); + continue; + } + + ALLOC_GROW(logs, logs_nr + 1, logs_alloc); + logs[logs_nr++] = log; + } + + /* + * We need to rewrite all reflog entries according to the pruning + * callback function: + * + * - If a reflog entry shall be pruned we mark the record for + * deletion. + * + * - Otherwise we may have to rewrite the chain of reflog entries so + * that gaps created by just-deleted records get backfilled. + */ + CALLOC_ARRAY(rewritten, logs_nr); + for (i = logs_nr; i--;) { + struct reftable_log_record *dest = &rewritten[i]; + struct object_id old_oid, new_oid; + + *dest = logs[i]; + oidread(&old_oid, logs[i].value.update.old_hash); + oidread(&new_oid, logs[i].value.update.new_hash); + + if (should_prune_fn(&old_oid, &new_oid, logs[i].value.update.email, + (timestamp_t)logs[i].value.update.time, + logs[i].value.update.tz_offset, + logs[i].value.update.message, + policy_cb_data)) { + dest->value_type = REFTABLE_LOG_DELETION; + } else { + if ((flags & EXPIRE_REFLOGS_REWRITE) && last_hash) + dest->value.update.old_hash = last_hash; + last_hash = logs[i].value.update.new_hash; + } + } + + if (flags & EXPIRE_REFLOGS_UPDATE_REF && last_hash && + reftable_ref_record_val1(&ref_record)) + oidread(&arg.update_oid, last_hash); + + arg.records = rewritten; + arg.len = logs_nr; + arg.stack = stack, + arg.refname = refname, + + ret = reftable_addition_add(add, &write_reflog_expiry_table, &arg); + if (ret < 0) + goto done; + + /* + * Future improvement: we could skip writing records that were + * not changed. + */ + if (!(flags & EXPIRE_REFLOGS_DRY_RUN)) + ret = reftable_addition_commit(add); + +done: + if (add) + cleanup_fn(policy_cb_data); + assert(ret != REFTABLE_API_ERROR); + + reftable_ref_record_release(&ref_record); + reftable_iterator_destroy(&it); + reftable_addition_destroy(add); + for (i = 0; i < logs_nr; i++) + reftable_log_record_release(&logs[i]); + free(logs); + free(rewritten); + return ret; +} + +struct ref_storage_be refs_be_reftable = { + .name = "reftable", + .init = reftable_be_init, + .init_db = reftable_be_init_db, + .transaction_prepare = reftable_be_transaction_prepare, + .transaction_finish = reftable_be_transaction_finish, + .transaction_abort = reftable_be_transaction_abort, + .initial_transaction_commit = reftable_be_initial_transaction_commit, + + .pack_refs = reftable_be_pack_refs, + .create_symref = reftable_be_create_symref, + .rename_ref = reftable_be_rename_ref, + .copy_ref = reftable_be_copy_ref, + + .iterator_begin = reftable_be_iterator_begin, + .read_raw_ref = reftable_be_read_raw_ref, + .read_symbolic_ref = reftable_be_read_symbolic_ref, + + .reflog_iterator_begin = reftable_be_reflog_iterator_begin, + .for_each_reflog_ent = reftable_be_for_each_reflog_ent, + .for_each_reflog_ent_reverse = reftable_be_for_each_reflog_ent_reverse, + .reflog_exists = reftable_be_reflog_exists, + .create_reflog = reftable_be_create_reflog, + .delete_reflog = reftable_be_delete_reflog, + .reflog_expire = reftable_be_reflog_expire, +}; diff --git a/reftable/block.c b/reftable/block.c index ce8a7d24f3..72eb73b380 100644 --- a/reftable/block.c +++ b/reftable/block.c @@ -339,8 +339,7 @@ int block_iter_next(struct block_iter *it, struct reftable_record *rec) return -1; string_view_consume(&in, n); - strbuf_reset(&it->last_key); - strbuf_addbuf(&it->last_key, &it->key); + strbuf_swap(&it->last_key, &it->key); it->next_off += start.len - in.len; return 0; } diff --git a/reftable/merged.c b/reftable/merged.c index a0f222e07b..1aa6cd31b7 100644 --- a/reftable/merged.c +++ b/reftable/merged.c @@ -49,8 +49,6 @@ static void merged_iter_close(void *p) for (size_t i = 0; i < mi->stack_len; i++) reftable_iterator_destroy(&mi->stack[i]); reftable_free(mi->stack); - strbuf_release(&mi->key); - strbuf_release(&mi->entry_key); } static int merged_iter_advance_nonnull_subiter(struct merged_iter *mi, @@ -105,14 +103,19 @@ static int merged_iter_next_entry(struct merged_iter *mi, such a deployment, the loop below must be changed to collect all entries for the same key, and return new the newest one. */ - reftable_record_key(&entry.rec, &mi->entry_key); while (!merged_iter_pqueue_is_empty(mi->pq)) { struct pq_entry top = merged_iter_pqueue_top(mi->pq); - int cmp = 0; - - reftable_record_key(&top.rec, &mi->key); + int cmp; + + /* + * When the next entry comes from the same queue as the current + * entry then it must by definition be larger. This avoids a + * comparison in the most common case. + */ + if (top.index == entry.index) + break; - cmp = strbuf_cmp(&mi->key, &mi->entry_key); + cmp = reftable_record_cmp(&top.rec, &entry.rec); if (cmp > 0) break; @@ -243,8 +246,6 @@ static int merged_table_seek_record(struct reftable_merged_table *mt, .typ = reftable_record_type(rec), .hash_id = mt->hash_id, .suppress_deletions = mt->suppress_deletions, - .key = STRBUF_INIT, - .entry_key = STRBUF_INIT, }; struct merged_iter *p; int err; diff --git a/reftable/merged.h b/reftable/merged.h index d5b39dfe7f..7d9f95d27e 100644 --- a/reftable/merged.h +++ b/reftable/merged.h @@ -31,8 +31,6 @@ struct merged_iter { uint8_t typ; int suppress_deletions; struct merged_iter_pqueue pq; - struct strbuf key; - struct strbuf entry_key; }; void merged_table_release(struct reftable_merged_table *mt); diff --git a/reftable/pq.c b/reftable/pq.c index 2461daf5ff..e0ccce2b97 100644 --- a/reftable/pq.c +++ b/reftable/pq.c @@ -14,20 +14,9 @@ https://developers.google.com/open-source/licenses/bsd int pq_less(struct pq_entry *a, struct pq_entry *b) { - struct strbuf ak = STRBUF_INIT; - struct strbuf bk = STRBUF_INIT; - int cmp = 0; - reftable_record_key(&a->rec, &ak); - reftable_record_key(&b->rec, &bk); - - cmp = strbuf_cmp(&ak, &bk); - - strbuf_release(&ak); - strbuf_release(&bk); - + int cmp = reftable_record_cmp(&a->rec, &b->rec); if (cmp == 0) return a->index > b->index; - return cmp < 0; } diff --git a/reftable/reader.c b/reftable/reader.c index 2663b03938..b113daab77 100644 --- a/reftable/reader.c +++ b/reftable/reader.c @@ -357,24 +357,32 @@ static int table_iter_next(struct table_iter *ti, struct reftable_record *rec) while (1) { struct table_iter next = TABLE_ITER_INIT; - int err = 0; - if (ti->is_finished) { + int err; + + if (ti->is_finished) return 1; - } + /* + * Check whether the current block still has more records. If + * so, return it. If the iterator returns positive then the + * current block has been exhausted. + */ err = table_iter_next_in_block(ti, rec); - if (err <= 0) { + if (err <= 0) return err; - } + /* + * Otherwise, we need to continue to the next block in the + * table and retry. If there are no more blocks then the + * iterator is drained. + */ err = table_iter_next_block(&next, ti); - if (err != 0) { - ti->is_finished = 1; - } table_iter_block_done(ti); - if (err != 0) { + if (err) { + ti->is_finished = 1; return err; } + table_iter_copy_from(ti, &next); block_iter_close(&next.bi); } diff --git a/reftable/record.c b/reftable/record.c index 26c5e43f9b..d6bb42e887 100644 --- a/reftable/record.c +++ b/reftable/record.c @@ -377,10 +377,11 @@ static int reftable_ref_record_decode(void *rec, struct strbuf key, assert(hash_size > 0); - r->refname = reftable_realloc(r->refname, key.len + 1); + r->refname = reftable_malloc(key.len + 1); memcpy(r->refname, key.buf, key.len); - r->update_index = update_index; r->refname[key.len] = 0; + + r->update_index = update_index; r->value_type = val_type; switch (val_type) { case REFTABLE_REF_VAL1: @@ -430,7 +431,6 @@ static int reftable_ref_record_is_deletion_void(const void *p) (const struct reftable_ref_record *)p); } - static int reftable_ref_record_equal_void(const void *a, const void *b, int hash_size) { @@ -439,6 +439,13 @@ static int reftable_ref_record_equal_void(const void *a, return reftable_ref_record_equal(ra, rb, hash_size); } +static int reftable_ref_record_cmp_void(const void *_a, const void *_b) +{ + const struct reftable_ref_record *a = _a; + const struct reftable_ref_record *b = _b; + return strcmp(a->refname, b->refname); +} + static void reftable_ref_record_print_void(const void *rec, int hash_size) { @@ -455,6 +462,7 @@ static struct reftable_record_vtable reftable_ref_record_vtable = { .release = &reftable_ref_record_release_void, .is_deletion = &reftable_ref_record_is_deletion_void, .equal = &reftable_ref_record_equal_void, + .cmp = &reftable_ref_record_cmp_void, .print = &reftable_ref_record_print_void, }; @@ -627,6 +635,25 @@ static int reftable_obj_record_equal_void(const void *a, const void *b, int hash return 1; } +static int reftable_obj_record_cmp_void(const void *_a, const void *_b) +{ + const struct reftable_obj_record *a = _a; + const struct reftable_obj_record *b = _b; + int cmp; + + cmp = memcmp(a->hash_prefix, b->hash_prefix, + a->hash_prefix_len > b->hash_prefix_len ? + a->hash_prefix_len : b->hash_prefix_len); + if (cmp) + return cmp; + + /* + * When the prefix is the same then the object record that is longer is + * considered to be bigger. + */ + return a->hash_prefix_len - b->hash_prefix_len; +} + static struct reftable_record_vtable reftable_obj_record_vtable = { .key = &reftable_obj_record_key, .type = BLOCK_TYPE_OBJ, @@ -637,6 +664,7 @@ static struct reftable_record_vtable reftable_obj_record_vtable = { .release = &reftable_obj_record_release, .is_deletion = ¬_a_deletion, .equal = &reftable_obj_record_equal_void, + .cmp = &reftable_obj_record_cmp_void, .print = &reftable_obj_record_print, }; @@ -955,6 +983,22 @@ static int reftable_log_record_equal_void(const void *a, hash_size); } +static int reftable_log_record_cmp_void(const void *_a, const void *_b) +{ + const struct reftable_log_record *a = _a; + const struct reftable_log_record *b = _b; + int cmp = strcmp(a->refname, b->refname); + if (cmp) + return cmp; + + /* + * Note that the comparison here is reversed. This is because the + * update index is reversed when comparing keys. For reference, see how + * we handle this in reftable_log_record_key()`. + */ + return b->update_index - a->update_index; +} + int reftable_log_record_equal(const struct reftable_log_record *a, const struct reftable_log_record *b, int hash_size) { @@ -1004,6 +1048,7 @@ static struct reftable_record_vtable reftable_log_record_vtable = { .release = &reftable_log_record_release_void, .is_deletion = &reftable_log_record_is_deletion_void, .equal = &reftable_log_record_equal_void, + .cmp = &reftable_log_record_cmp_void, .print = &reftable_log_record_print_void, }; @@ -1079,6 +1124,13 @@ static int reftable_index_record_equal(const void *a, const void *b, int hash_si return ia->offset == ib->offset && !strbuf_cmp(&ia->last_key, &ib->last_key); } +static int reftable_index_record_cmp(const void *_a, const void *_b) +{ + const struct reftable_index_record *a = _a; + const struct reftable_index_record *b = _b; + return strbuf_cmp(&a->last_key, &b->last_key); +} + static void reftable_index_record_print(const void *rec, int hash_size) { const struct reftable_index_record *idx = rec; @@ -1096,6 +1148,7 @@ static struct reftable_record_vtable reftable_index_record_vtable = { .release = &reftable_index_record_release, .is_deletion = ¬_a_deletion, .equal = &reftable_index_record_equal, + .cmp = &reftable_index_record_cmp, .print = &reftable_index_record_print, }; @@ -1149,6 +1202,14 @@ int reftable_record_is_deletion(struct reftable_record *rec) reftable_record_data(rec)); } +int reftable_record_cmp(struct reftable_record *a, struct reftable_record *b) +{ + if (a->type != b->type) + BUG("cannot compare reftable records of different type"); + return reftable_record_vtable(a)->cmp( + reftable_record_data(a), reftable_record_data(b)); +} + int reftable_record_equal(struct reftable_record *a, struct reftable_record *b, int hash_size) { if (a->type != b->type) diff --git a/reftable/record.h b/reftable/record.h index e64ed30c80..a05e2be179 100644 --- a/reftable/record.h +++ b/reftable/record.h @@ -62,6 +62,12 @@ struct reftable_record_vtable { /* Are two records equal? This assumes they have the same type. Returns 0 for non-equal. */ int (*equal)(const void *a, const void *b, int hash_size); + /* + * Compare keys of two records with each other. The records must have + * the same type. + */ + int (*cmp)(const void *a, const void *b); + /* Print on stdout, for debugging. */ void (*print)(const void *rec, int hash_size); }; @@ -114,6 +120,7 @@ struct reftable_record { void reftable_record_init(struct reftable_record *rec, uint8_t typ); /* see struct record_vtable */ +int reftable_record_cmp(struct reftable_record *a, struct reftable_record *b); int reftable_record_equal(struct reftable_record *a, struct reftable_record *b, int hash_size); void reftable_record_print(struct reftable_record *rec, int hash_size); void reftable_record_key(struct reftable_record *rec, struct strbuf *dest); @@ -105,7 +105,7 @@ static int remotes_hash_cmp(const void *cmp_data UNUSED, b = container_of(entry_or_key, const struct remote, ent); if (key) - return strncmp(a->name, key->str, key->len) || a->name[key->len]; + return !!xstrncmpz(a->name, key->str, key->len); else return strcmp(a->name, b->name); } @@ -189,8 +189,7 @@ static int branches_hash_cmp(const void *cmp_data UNUSED, b = container_of(entry_or_key, const struct branch, ent); if (key) - return strncmp(a->name, key->str, key->len) || - a->name[key->len]; + return !!xstrncmpz(a->name, key->str, key->len); else return strcmp(a->name, b->name); } diff --git a/repository.h b/repository.h index 21949c5a17..9bf1e33d25 100644 --- a/repository.h +++ b/repository.h @@ -24,8 +24,9 @@ enum fetch_negotiation_setting { FETCH_NEGOTIATION_NOOP, }; -#define REF_STORAGE_FORMAT_UNKNOWN 0 -#define REF_STORAGE_FORMAT_FILES 1 +#define REF_STORAGE_FORMAT_UNKNOWN 0 +#define REF_STORAGE_FORMAT_FILES 1 +#define REF_STORAGE_FORMAT_REFTABLE 2 struct repo_settings { int initialized; diff --git a/t/lib-credential.sh b/t/lib-credential.sh index 15fc9a31e2..44799c0d38 100644 --- a/t/lib-credential.sh +++ b/t/lib-credential.sh @@ -50,6 +50,7 @@ helper_test_clean() { reject $1 https example.com user-overwrite reject $1 https example.com user-erase1 reject $1 https example.com user-erase2 + reject $1 https victim.example.com user reject $1 http path.tld user reject $1 https timeout.tld user reject $1 https sso.tld diff --git a/t/t0303-credential-external.sh b/t/t0303-credential-external.sh index 095574bfc6..72ae405c3e 100755 --- a/t/t0303-credential-external.sh +++ b/t/t0303-credential-external.sh @@ -32,9 +32,24 @@ commands. . ./test-lib.sh . "$TEST_DIRECTORY"/lib-credential.sh +# If we're not given a specific external helper to run against, +# there isn't much to test. But we can still run through our +# battery of tests with a fake helper and check that the +# test themselves are self-consistent and clean up after +# themselves. +# +# We'll use the "store" helper, since we can easily inspect +# its state by looking at the on-disk file. But since it doesn't +# implement any caching or expiry logic, we'll cheat and override +# the "check" function to just report all results as OK. if test -z "$GIT_TEST_CREDENTIAL_HELPER"; then - skip_all="used to test external credential helpers" - test_done + GIT_TEST_CREDENTIAL_HELPER=store + GIT_TEST_CREDENTIAL_HELPER_TIMEOUT=store + check () { + test "$1" = "approve" || return 0 + git -c credential.helper=store credential approve + } + check_cleanup=t fi test -z "$GIT_TEST_CREDENTIAL_HELPER_SETUP" || @@ -59,4 +74,11 @@ fi # might be long-term system storage helper_test_clean "$GIT_TEST_CREDENTIAL_HELPER" +if test "$check_cleanup" = "t" +then + test_expect_success 'test cleanup removes everything' ' + test_must_be_empty "$HOME/.git-credentials" + ' +fi + test_done diff --git a/t/t0410-partial-clone.sh b/t/t0410-partial-clone.sh index 6b6424b3df..0f98b21be8 100755 --- a/t/t0410-partial-clone.sh +++ b/t/t0410-partial-clone.sh @@ -49,7 +49,7 @@ test_expect_success 'convert shallow clone to partial clone' ' test_cmp_config -C client 1 core.repositoryformatversion ' -test_expect_success SHA1,REFFILES 'convert to partial clone with noop extension' ' +test_expect_success DEFAULT_REPO_FORMAT 'convert to partial clone with noop extension' ' rm -fr server client && test_create_repo server && test_commit -C server my_commit 1 && @@ -60,7 +60,7 @@ test_expect_success SHA1,REFFILES 'convert to partial clone with noop extension' git -C client fetch --unshallow --filter="blob:none" ' -test_expect_success SHA1,REFFILES 'converting to partial clone fails with unrecognized extension' ' +test_expect_success DEFAULT_REPO_FORMAT 'converting to partial clone fails with unrecognized extension' ' rm -fr server client && test_create_repo server && test_commit -C server my_commit 1 && diff --git a/t/t0600-reffiles-backend.sh b/t/t0600-reffiles-backend.sh index e6a5f1868f..485481d6b4 100755 --- a/t/t0600-reffiles-backend.sh +++ b/t/t0600-reffiles-backend.sh @@ -381,4 +381,95 @@ test_expect_success 'log diagnoses bogus HEAD symref' ' test_grep broken stderr ' +test_expect_success 'empty directory removal' ' + git branch d1/d2/r1 HEAD && + git branch d1/r2 HEAD && + test_path_is_file .git/refs/heads/d1/d2/r1 && + test_path_is_file .git/logs/refs/heads/d1/d2/r1 && + git branch -d d1/d2/r1 && + test_must_fail git show-ref --verify -q refs/heads/d1/d2 && + test_must_fail git show-ref --verify -q logs/refs/heads/d1/d2 && + test_path_is_file .git/refs/heads/d1/r2 && + test_path_is_file .git/logs/refs/heads/d1/r2 +' + +test_expect_success 'symref empty directory removal' ' + git branch e1/e2/r1 HEAD && + git branch e1/r2 HEAD && + git checkout e1/e2/r1 && + test_when_finished "git checkout main" && + test_path_is_file .git/refs/heads/e1/e2/r1 && + test_path_is_file .git/logs/refs/heads/e1/e2/r1 && + git update-ref -d HEAD && + test_must_fail git show-ref --verify -q refs/heads/e1/e2 && + test_must_fail git show-ref --verify -q logs/refs/heads/e1/e2 && + test_path_is_file .git/refs/heads/e1/r2 && + test_path_is_file .git/logs/refs/heads/e1/r2 && + test_path_is_file .git/logs/HEAD +' + +test_expect_success 'directory not created deleting packed ref' ' + git branch d1/d2/r1 HEAD && + git pack-refs --all && + test_path_is_missing .git/refs/heads/d1/d2 && + git update-ref -d refs/heads/d1/d2/r1 && + test_path_is_missing .git/refs/heads/d1/d2 && + test_path_is_missing .git/refs/heads/d1 +' + +test_expect_success SYMLINKS 'git branch -m u v should fail when the reflog for u is a symlink' ' + git branch --create-reflog u && + mv .git/logs/refs/heads/u real-u && + ln -s real-u .git/logs/refs/heads/u && + test_must_fail git branch -m u v +' + +test_expect_success SYMLINKS 'git branch -m with symlinked .git/refs' ' + test_when_finished "rm -rf subdir" && + git init --bare subdir && + + rm -rfv subdir/refs subdir/objects subdir/packed-refs && + ln -s ../.git/refs subdir/refs && + ln -s ../.git/objects subdir/objects && + ln -s ../.git/packed-refs subdir/packed-refs && + + git -C subdir rev-parse --absolute-git-dir >subdir.dir && + git rev-parse --absolute-git-dir >our.dir && + ! test_cmp subdir.dir our.dir && + + git -C subdir log && + git -C subdir branch rename-src && + git rev-parse rename-src >expect && + git -C subdir branch -m rename-src rename-dest && + git rev-parse rename-dest >actual && + test_cmp expect actual && + git branch -D rename-dest +' + +test_expect_success MINGW,SYMLINKS_WINDOWS 'rebase when .git/logs is a symlink' ' + git checkout main && + mv .git/logs actual_logs && + cmd //c "mklink /D .git\logs ..\actual_logs" && + git rebase -f HEAD^ && + test -L .git/logs && + rm .git/logs && + mv actual_logs .git/logs +' + +test_expect_success POSIXPERM 'git reflog expire honors core.sharedRepository' ' + umask 077 && + git config core.sharedRepository group && + git reflog expire --all && + actual="$(ls -l .git/logs/refs/heads/main)" && + case "$actual" in + -rw-rw-*) + : happy + ;; + *) + echo Ooops, .git/logs/refs/heads/main is not 066x [$actual] + false + ;; + esac +' + test_done diff --git a/t/t0610-reftable-basics.sh b/t/t0610-reftable-basics.sh new file mode 100755 index 0000000000..6a131e40b8 --- /dev/null +++ b/t/t0610-reftable-basics.sh @@ -0,0 +1,887 @@ +#!/bin/sh +# +# Copyright (c) 2020 Google LLC +# + +test_description='reftable basics' +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main +export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME + +. ./test-lib.sh + +if ! test_have_prereq REFTABLE +then + skip_all='skipping reftable tests; set GIT_TEST_DEFAULT_REF_FORMAT=reftable' + test_done +fi + +INVALID_OID=$(test_oid 001) + +test_expect_success 'init: creates basic reftable structures' ' + test_when_finished "rm -rf repo" && + git init repo && + test_path_is_dir repo/.git/reftable && + test_path_is_file repo/.git/reftable/tables.list && + echo reftable >expect && + git -C repo rev-parse --show-ref-format >actual && + test_cmp expect actual +' + +test_expect_success 'init: sha256 object format via environment variable' ' + test_when_finished "rm -rf repo" && + GIT_DEFAULT_HASH=sha256 git init repo && + cat >expect <<-EOF && + sha256 + reftable + EOF + git -C repo rev-parse --show-object-format --show-ref-format >actual && + test_cmp expect actual +' + +test_expect_success 'init: sha256 object format via option' ' + test_when_finished "rm -rf repo" && + git init --object-format=sha256 repo && + cat >expect <<-EOF && + sha256 + reftable + EOF + git -C repo rev-parse --show-object-format --show-ref-format >actual && + test_cmp expect actual +' + +test_expect_success 'init: reinitializing reftable backend succeeds' ' + test_when_finished "rm -rf repo" && + git init repo && + test_commit -C repo A && + + git -C repo for-each-ref >expect && + git init --ref-format=reftable repo && + git -C repo for-each-ref >actual && + test_cmp expect actual +' + +test_expect_success 'init: reinitializing files with reftable backend fails' ' + test_when_finished "rm -rf repo" && + git init --ref-format=files repo && + test_commit -C repo file && + + cp repo/.git/HEAD expect && + test_must_fail git init --ref-format=reftable repo && + test_cmp expect repo/.git/HEAD +' + +test_expect_success 'init: reinitializing reftable with files backend fails' ' + test_when_finished "rm -rf repo" && + git init --ref-format=reftable repo && + test_commit -C repo file && + + cp repo/.git/HEAD expect && + test_must_fail git init --ref-format=files repo && + test_cmp expect repo/.git/HEAD +' + +test_expect_perms () { + local perms="$1" + local file="$2" + local actual=$(ls -l "$file") && + + case "$actual" in + $perms*) + : happy + ;; + *) + echo "$(basename $2) is not $perms but $actual" + false + ;; + esac +} + +for umask in 002 022 +do + test_expect_success POSIXPERM 'init: honors core.sharedRepository' ' + test_when_finished "rm -rf repo" && + ( + umask $umask && + git init --shared=true repo && + test 1 = "$(git -C repo config core.sharedrepository)" + ) && + test_expect_perms "-rw-rw-r--" repo/.git/reftable/tables.list && + for table in repo/.git/reftable/*.ref + do + test_expect_perms "-rw-rw-r--" "$table" || + return 1 + done + ' +done + +test_expect_success 'clone: can clone reftable repository' ' + test_when_finished "rm -rf repo clone" && + git init repo && + test_commit -C repo message1 file1 && + + git clone repo cloned && + echo reftable >expect && + git -C cloned rev-parse --show-ref-format >actual && + test_cmp expect actual && + test_path_is_file cloned/file1 +' + +test_expect_success 'clone: can clone reffiles into reftable repository' ' + test_when_finished "rm -rf reffiles reftable" && + git init --ref-format=files reffiles && + test_commit -C reffiles A && + git clone --ref-format=reftable ./reffiles reftable && + + git -C reffiles rev-parse HEAD >expect && + git -C reftable rev-parse HEAD >actual && + test_cmp expect actual && + + git -C reftable rev-parse --show-ref-format >actual && + echo reftable >expect && + test_cmp expect actual && + + git -C reffiles rev-parse --show-ref-format >actual && + echo files >expect && + test_cmp expect actual +' + +test_expect_success 'clone: can clone reftable into reffiles repository' ' + test_when_finished "rm -rf reffiles reftable" && + git init --ref-format=reftable reftable && + test_commit -C reftable A && + git clone --ref-format=files ./reftable reffiles && + + git -C reftable rev-parse HEAD >expect && + git -C reffiles rev-parse HEAD >actual && + test_cmp expect actual && + + git -C reftable rev-parse --show-ref-format >actual && + echo reftable >expect && + test_cmp expect actual && + + git -C reffiles rev-parse --show-ref-format >actual && + echo files >expect && + test_cmp expect actual +' + +test_expect_success 'ref transaction: corrupted tables cause failure' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + test_commit file1 && + for f in .git/reftable/*.ref + do + : >"$f" || return 1 + done && + test_must_fail git update-ref refs/heads/main HEAD + ) +' + +test_expect_success 'ref transaction: corrupted tables.list cause failure' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + test_commit file1 && + echo garbage >.git/reftable/tables.list && + test_must_fail git update-ref refs/heads/main HEAD + ) +' + +test_expect_success 'ref transaction: refuses to write ref causing F/D conflict' ' + test_when_finished "rm -rf repo" && + git init repo && + test_commit -C repo file && + test_must_fail git -C repo update-ref refs/heads/main/forbidden +' + +test_expect_success 'ref transaction: deleting ref with invalid name fails' ' + test_when_finished "rm -rf repo" && + git init repo && + test_commit -C repo file && + test_must_fail git -C repo update-ref -d ../../my-private-file +' + +test_expect_success 'ref transaction: can skip object ID verification' ' + test_when_finished "rm -rf repo" && + git init repo && + test_must_fail test-tool -C repo ref-store main update-ref msg refs/heads/branch $INVALID_OID $ZERO_OID 0 && + test-tool -C repo ref-store main update-ref msg refs/heads/branch $INVALID_OID $ZERO_OID REF_SKIP_OID_VERIFICATION +' + +test_expect_success 'ref transaction: updating same ref multiple times fails' ' + test_when_finished "rm -rf repo" && + git init repo && + test_commit -C repo A && + cat >updates <<-EOF && + update refs/heads/main $A + update refs/heads/main $A + EOF + cat >expect <<-EOF && + fatal: multiple updates for ref ${SQ}refs/heads/main${SQ} not allowed + EOF + test_must_fail git -C repo update-ref --stdin <updates 2>err && + test_cmp expect err +' + +test_expect_success 'ref transaction: can delete symbolic self-reference with git-symbolic-ref(1)' ' + test_when_finished "rm -rf repo" && + git init repo && + git -C repo symbolic-ref refs/heads/self refs/heads/self && + git -C repo symbolic-ref -d refs/heads/self +' + +test_expect_success 'ref transaction: deleting symbolic self-reference without --no-deref fails' ' + test_when_finished "rm -rf repo" && + git init repo && + git -C repo symbolic-ref refs/heads/self refs/heads/self && + cat >expect <<-EOF && + error: multiple updates for ${SQ}refs/heads/self${SQ} (including one via symref ${SQ}refs/heads/self${SQ}) are not allowed + EOF + test_must_fail git -C repo update-ref -d refs/heads/self 2>err && + test_cmp expect err +' + +test_expect_success 'ref transaction: deleting symbolic self-reference with --no-deref succeeds' ' + test_when_finished "rm -rf repo" && + git init repo && + git -C repo symbolic-ref refs/heads/self refs/heads/self && + git -C repo update-ref -d --no-deref refs/heads/self +' + +test_expect_success 'ref transaction: creating symbolic ref fails with F/D conflict' ' + test_when_finished "rm -rf repo" && + git init repo && + test_commit -C repo A && + cat >expect <<-EOF && + error: unable to write symref for refs/heads: file/directory conflict + EOF + test_must_fail git -C repo symbolic-ref refs/heads refs/heads/foo 2>err && + test_cmp expect err +' + +test_expect_success 'ref transaction: ref deletion' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + test_commit file && + HEAD_OID=$(git show-ref -s --verify HEAD) && + cat >expect <<-EOF && + $HEAD_OID refs/heads/main + $HEAD_OID refs/tags/file + EOF + git show-ref >actual && + test_cmp expect actual && + + test_must_fail git update-ref -d refs/tags/file $INVALID_OID && + git show-ref >actual && + test_cmp expect actual && + + git update-ref -d refs/tags/file $HEAD_OID && + echo "$HEAD_OID refs/heads/main" >expect && + git show-ref >actual && + test_cmp expect actual + ) +' + +test_expect_success 'ref transaction: writes cause auto-compaction' ' + test_when_finished "rm -rf repo" && + + git init repo && + test_line_count = 1 repo/.git/reftable/tables.list && + + test_commit -C repo --no-tag A && + test_line_count = 2 repo/.git/reftable/tables.list && + + test_commit -C repo --no-tag B && + test_line_count = 1 repo/.git/reftable/tables.list +' + +check_fsync_events () { + local trace="$1" && + shift && + + cat >expect && + sed -n \ + -e '/^{"event":"counter",.*"category":"fsync",/ { + s/.*"category":"fsync",//; + s/}$//; + p; + }' \ + <"$trace" >actual && + test_cmp expect actual +} + +test_expect_success 'ref transaction: writes are synced' ' + test_when_finished "rm -rf repo" && + git init repo && + test_commit -C repo initial && + + GIT_TRACE2_EVENT="$(pwd)/trace2.txt" \ + GIT_TEST_FSYNC=true \ + git -C repo -c core.fsync=reference \ + -c core.fsyncMethod=fsync update-ref refs/heads/branch HEAD && + check_fsync_events trace2.txt <<-EOF + "name":"hardware-flush","count":2 + EOF +' + +test_expect_success 'pack-refs: compacts tables' ' + test_when_finished "rm -rf repo" && + git init repo && + + test_commit -C repo A && + ls -1 repo/.git/reftable >table-files && + test_line_count = 4 table-files && + test_line_count = 3 repo/.git/reftable/tables.list && + + git -C repo pack-refs && + ls -1 repo/.git/reftable >table-files && + test_line_count = 2 table-files && + test_line_count = 1 repo/.git/reftable/tables.list +' + +test_expect_success 'pack-refs: prunes stale tables' ' + test_when_finished "rm -rf repo" && + git init repo && + touch repo/.git/reftable/stale-table.ref && + git -C repo pack-refs && + test_path_is_missing repo/.git/reftable/stable-ref.ref +' + +test_expect_success 'pack-refs: does not prune non-table files' ' + test_when_finished "rm -rf repo" && + git init repo && + touch repo/.git/reftable/garbage && + git -C repo pack-refs && + test_path_is_file repo/.git/reftable/garbage +' + +for umask in 002 022 +do + test_expect_success POSIXPERM 'pack-refs: honors core.sharedRepository' ' + test_when_finished "rm -rf repo" && + ( + umask $umask && + git init --shared=true repo && + test_commit -C repo A && + test_line_count = 3 repo/.git/reftable/tables.list + ) && + git -C repo pack-refs && + test_expect_perms "-rw-rw-r--" repo/.git/reftable/tables.list && + for table in repo/.git/reftable/*.ref + do + test_expect_perms "-rw-rw-r--" "$table" || + return 1 + done + ' +done + +test_expect_success 'packed-refs: writes are synced' ' + test_when_finished "rm -rf repo" && + git init repo && + test_commit -C repo initial && + test_line_count = 2 table-files && + + : >trace2.txt && + GIT_TRACE2_EVENT="$(pwd)/trace2.txt" \ + GIT_TEST_FSYNC=true \ + git -C repo -c core.fsync=reference \ + -c core.fsyncMethod=fsync pack-refs && + check_fsync_events trace2.txt <<-EOF + "name":"hardware-flush","count":2 + EOF +' + +test_expect_success 'ref iterator: bogus names are flagged' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + test_commit --no-tag file && + test-tool ref-store main update-ref msg "refs/heads/bogus..name" $(git rev-parse HEAD) $ZERO_OID REF_SKIP_REFNAME_VERIFICATION && + + cat >expect <<-EOF && + $ZERO_OID refs/heads/bogus..name 0xc + $(git rev-parse HEAD) refs/heads/main 0x0 + EOF + test-tool ref-store main for-each-ref "" >actual && + test_cmp expect actual + ) +' + +test_expect_success 'ref iterator: missing object IDs are not flagged' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + test-tool ref-store main update-ref msg "refs/heads/broken-hash" $INVALID_OID $ZERO_OID REF_SKIP_OID_VERIFICATION && + + cat >expect <<-EOF && + $INVALID_OID refs/heads/broken-hash 0x0 + EOF + test-tool ref-store main for-each-ref "" >actual && + test_cmp expect actual + ) +' + +test_expect_success 'basic: commit and list refs' ' + test_when_finished "rm -rf repo" && + git init repo && + test_commit -C repo file && + test_write_lines refs/heads/main refs/tags/file >expect && + git -C repo for-each-ref --format="%(refname)" >actual && + test_cmp actual expect +' + +test_expect_success 'basic: can write large commit message' ' + test_when_finished "rm -rf repo" && + git init repo && + perl -e " + print \"this is a long commit message\" x 50000 + " >commit-msg && + git -C repo commit --allow-empty --file=../commit-msg +' + +test_expect_success 'basic: show-ref fails with empty repository' ' + test_when_finished "rm -rf repo" && + git init repo && + test_must_fail git -C repo show-ref >actual && + test_must_be_empty actual +' + +test_expect_success 'basic: can check out unborn branch' ' + test_when_finished "rm -rf repo" && + git init repo && + git -C repo checkout -b main +' + +test_expect_success 'basic: peeled tags are stored' ' + test_when_finished "rm -rf repo" && + git init repo && + test_commit -C repo file && + git -C repo tag -m "annotated tag" test_tag HEAD && + for ref in refs/heads/main refs/tags/file refs/tags/test_tag refs/tags/test_tag^{} + do + echo "$(git -C repo rev-parse "$ref") $ref" || return 1 + done >expect && + git -C repo show-ref -d >actual && + test_cmp expect actual +' + +test_expect_success 'basic: for-each-ref can print symrefs' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + test_commit file && + git branch && + git symbolic-ref refs/heads/sym refs/heads/main && + cat >expected <<-EOF && + refs/heads/main + EOF + git for-each-ref --format="%(symref)" refs/heads/sym >actual && + test_cmp expected actual + ) +' + +test_expect_success 'basic: notes' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + write_script fake_editor <<-\EOF && + echo "$MSG" >"$1" + echo "$MSG" >&2 + EOF + + test_commit 1st && + test_commit 2nd && + GIT_EDITOR=./fake_editor MSG=b4 git notes add && + GIT_EDITOR=./fake_editor MSG=b3 git notes edit && + echo b4 >expect && + git notes --ref commits@{1} show >actual && + test_cmp expect actual + ) +' + +test_expect_success 'basic: stash' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + test_commit file && + git stash list >expect && + test_line_count = 0 expect && + + echo hoi >>file.t && + git stash push -m stashed && + git stash list >expect && + test_line_count = 1 expect && + + git stash clear && + git stash list >expect && + test_line_count = 0 expect + ) +' + +test_expect_success 'basic: cherry-pick' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + test_commit message1 file1 && + test_commit message2 file2 && + git branch source && + git checkout HEAD^ && + test_commit message3 file3 && + git cherry-pick source && + test_path_is_file file2 + ) +' + +test_expect_success 'basic: rebase' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + test_commit message1 file1 && + test_commit message2 file2 && + git branch source && + git checkout HEAD^ && + test_commit message3 file3 && + git rebase source && + test_path_is_file file2 + ) +' + +test_expect_success 'reflog: can delete separate reflog entries' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + + test_commit file && + test_commit file2 && + test_commit file3 && + test_commit file4 && + git reflog >actual && + grep file3 actual && + + git reflog delete HEAD@{1} && + git reflog >actual && + ! grep file3 actual + ) +' + +test_expect_success 'reflog: can switch to previous branch' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + test_commit file1 && + git checkout -b branch1 && + test_commit file2 && + git checkout -b branch2 && + git switch - && + git rev-parse --symbolic-full-name HEAD >actual && + echo refs/heads/branch1 >expect && + test_cmp actual expect + ) +' + +test_expect_success 'reflog: copying branch writes reflog entry' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + test_commit file1 && + test_commit file2 && + oid=$(git rev-parse --short HEAD) && + git branch src && + cat >expect <<-EOF && + ${oid} dst@{0}: Branch: copied refs/heads/src to refs/heads/dst + ${oid} dst@{1}: branch: Created from main + EOF + git branch -c src dst && + git reflog dst >actual && + test_cmp expect actual + ) +' + +test_expect_success 'reflog: renaming branch writes reflog entry' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + git symbolic-ref HEAD refs/heads/before && + test_commit file && + git show-ref >expected.refs && + sed s/before/after/g <expected.refs >expected && + git branch -M after && + git show-ref >actual && + test_cmp expected actual && + echo refs/heads/after >expected && + git symbolic-ref HEAD >actual && + test_cmp expected actual + ) +' + +test_expect_success 'reflog: can store empty logs' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + + test_must_fail test-tool ref-store main reflog-exists refs/heads/branch && + test-tool ref-store main create-reflog refs/heads/branch && + test-tool ref-store main reflog-exists refs/heads/branch && + test-tool ref-store main for-each-reflog-ent-reverse refs/heads/branch >actual && + test_must_be_empty actual + ) +' + +test_expect_success 'reflog: expiry empties reflog' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + + test_commit initial && + git checkout -b branch && + test_commit fileA && + test_commit fileB && + + cat >expect <<-EOF && + commit: fileB + commit: fileA + branch: Created from HEAD + EOF + git reflog show --format="%gs" refs/heads/branch >actual && + test_cmp expect actual && + + git reflog expire branch --expire=all && + git reflog show --format="%gs" refs/heads/branch >actual && + test_must_be_empty actual && + test-tool ref-store main reflog-exists refs/heads/branch + ) +' + +test_expect_success 'reflog: can be deleted' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + test_commit initial && + test-tool ref-store main reflog-exists refs/heads/main && + test-tool ref-store main delete-reflog refs/heads/main && + test_must_fail test-tool ref-store main reflog-exists refs/heads/main + ) +' + +test_expect_success 'reflog: garbage collection deletes reflog entries' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + + for count in $(test_seq 1 10) + do + test_commit "number $count" file.t $count number-$count || + return 1 + done && + git reflog refs/heads/main >actual && + test_line_count = 10 actual && + grep "commit (initial): number 1" actual && + grep "commit: number 10" actual && + + git gc && + git reflog refs/heads/main >actual && + test_line_count = 0 actual + ) +' + +test_expect_success 'reflog: updates via HEAD update HEAD reflog' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + test_commit main-one && + git checkout -b new-branch && + test_commit new-one && + test_commit new-two && + + echo new-one >expect && + git log -1 --format=%s HEAD@{1} >actual && + test_cmp expect actual + ) +' + +test_expect_success 'worktree: adding worktree creates separate stack' ' + test_when_finished "rm -rf repo worktree" && + git init repo && + test_commit -C repo A && + + git -C repo worktree add ../worktree && + test_path_is_file repo/.git/worktrees/worktree/refs/heads && + echo "ref: refs/heads/.invalid" >expect && + test_cmp expect repo/.git/worktrees/worktree/HEAD && + test_path_is_dir repo/.git/worktrees/worktree/reftable && + test_path_is_file repo/.git/worktrees/worktree/reftable/tables.list +' + +test_expect_success 'worktree: pack-refs in main repo packs main refs' ' + test_when_finished "rm -rf repo worktree" && + git init repo && + test_commit -C repo A && + git -C repo worktree add ../worktree && + + test_line_count = 3 repo/.git/worktrees/worktree/reftable/tables.list && + test_line_count = 4 repo/.git/reftable/tables.list && + git -C repo pack-refs && + test_line_count = 3 repo/.git/worktrees/worktree/reftable/tables.list && + test_line_count = 1 repo/.git/reftable/tables.list +' + +test_expect_success 'worktree: pack-refs in worktree packs worktree refs' ' + test_when_finished "rm -rf repo worktree" && + git init repo && + test_commit -C repo A && + git -C repo worktree add ../worktree && + + test_line_count = 3 repo/.git/worktrees/worktree/reftable/tables.list && + test_line_count = 4 repo/.git/reftable/tables.list && + git -C worktree pack-refs && + test_line_count = 1 repo/.git/worktrees/worktree/reftable/tables.list && + test_line_count = 4 repo/.git/reftable/tables.list +' + +test_expect_success 'worktree: creating shared ref updates main stack' ' + test_when_finished "rm -rf repo worktree" && + git init repo && + test_commit -C repo A && + + git -C repo worktree add ../worktree && + git -C repo pack-refs && + git -C worktree pack-refs && + test_line_count = 1 repo/.git/worktrees/worktree/reftable/tables.list && + test_line_count = 1 repo/.git/reftable/tables.list && + + git -C worktree update-ref refs/heads/shared HEAD && + test_line_count = 1 repo/.git/worktrees/worktree/reftable/tables.list && + test_line_count = 2 repo/.git/reftable/tables.list +' + +test_expect_success 'worktree: creating per-worktree ref updates worktree stack' ' + test_when_finished "rm -rf repo worktree" && + git init repo && + test_commit -C repo A && + + git -C repo worktree add ../worktree && + git -C repo pack-refs && + git -C worktree pack-refs && + test_line_count = 1 repo/.git/worktrees/worktree/reftable/tables.list && + test_line_count = 1 repo/.git/reftable/tables.list && + + git -C worktree update-ref refs/bisect/per-worktree HEAD && + test_line_count = 2 repo/.git/worktrees/worktree/reftable/tables.list && + test_line_count = 1 repo/.git/reftable/tables.list +' + +test_expect_success 'worktree: creating per-worktree ref from main repo' ' + test_when_finished "rm -rf repo worktree" && + git init repo && + test_commit -C repo A && + + git -C repo worktree add ../worktree && + git -C repo pack-refs && + git -C worktree pack-refs && + test_line_count = 1 repo/.git/worktrees/worktree/reftable/tables.list && + test_line_count = 1 repo/.git/reftable/tables.list && + + git -C repo update-ref worktrees/worktree/refs/bisect/per-worktree HEAD && + test_line_count = 2 repo/.git/worktrees/worktree/reftable/tables.list && + test_line_count = 1 repo/.git/reftable/tables.list +' + +test_expect_success 'worktree: creating per-worktree ref from second worktree' ' + test_when_finished "rm -rf repo wt1 wt2" && + git init repo && + test_commit -C repo A && + + git -C repo worktree add ../wt1 && + git -C repo worktree add ../wt2 && + git -C repo pack-refs && + git -C wt1 pack-refs && + git -C wt2 pack-refs && + test_line_count = 1 repo/.git/worktrees/wt1/reftable/tables.list && + test_line_count = 1 repo/.git/worktrees/wt2/reftable/tables.list && + test_line_count = 1 repo/.git/reftable/tables.list && + + git -C wt1 update-ref worktrees/wt2/refs/bisect/per-worktree HEAD && + test_line_count = 1 repo/.git/worktrees/wt1/reftable/tables.list && + test_line_count = 2 repo/.git/worktrees/wt2/reftable/tables.list && + test_line_count = 1 repo/.git/reftable/tables.list +' + +test_expect_success 'worktree: can create shared and per-worktree ref in one transaction' ' + test_when_finished "rm -rf repo worktree" && + git init repo && + test_commit -C repo A && + + git -C repo worktree add ../worktree && + git -C repo pack-refs && + git -C worktree pack-refs && + test_line_count = 1 repo/.git/worktrees/worktree/reftable/tables.list && + test_line_count = 1 repo/.git/reftable/tables.list && + + cat >stdin <<-EOF && + create worktrees/worktree/refs/bisect/per-worktree HEAD + create refs/branches/shared HEAD + EOF + git -C repo update-ref --stdin <stdin && + test_line_count = 2 repo/.git/worktrees/worktree/reftable/tables.list && + test_line_count = 2 repo/.git/reftable/tables.list +' + +test_expect_success 'worktree: can access common refs' ' + test_when_finished "rm -rf repo worktree" && + git init repo && + test_commit -C repo file1 && + git -C repo branch branch1 && + git -C repo worktree add ../worktree && + + echo refs/heads/worktree >expect && + git -C worktree symbolic-ref HEAD >actual && + test_cmp expect actual && + git -C worktree checkout branch1 +' + +test_expect_success 'worktree: adds worktree with detached HEAD' ' + test_when_finished "rm -rf repo worktree" && + + git init repo && + test_commit -C repo A && + git -C repo rev-parse main >expect && + + git -C repo worktree add --detach ../worktree main && + git -C worktree rev-parse HEAD >actual && + test_cmp expect actual +' + +test_expect_success 'fetch: accessing FETCH_HEAD special ref works' ' + test_when_finished "rm -rf repo sub" && + + git init sub && + test_commit -C sub two && + git -C sub rev-parse HEAD >expect && + + git init repo && + test_commit -C repo one && + git -C repo fetch ../sub && + git -C repo rev-parse FETCH_HEAD >actual && + test_cmp expect actual +' + +test_done diff --git a/t/t0611-reftable-httpd.sh b/t/t0611-reftable-httpd.sh new file mode 100755 index 0000000000..5e05b9c1f2 --- /dev/null +++ b/t/t0611-reftable-httpd.sh @@ -0,0 +1,26 @@ +#!/bin/sh + +test_description='reftable HTTPD tests' + +. ./test-lib.sh +. "$TEST_DIRECTORY"/lib-httpd.sh + +start_httpd + +REPO="$HTTPD_DOCUMENT_ROOT_PATH/repo" + +test_expect_success 'serving ls-remote' ' + git init --ref-format=reftable -b main "$REPO" && + cd "$REPO" && + test_commit m1 && + >.git/git-daemon-export-ok && + git ls-remote "http://127.0.0.1:$LIB_HTTPD_PORT/smart/repo" | cut -f 2-2 -d " " >actual && + cat >expect <<-EOF && + HEAD + refs/heads/main + refs/tags/m1 + EOF + test_cmp actual expect +' + +test_done diff --git a/t/t1301-shared-repo.sh b/t/t1301-shared-repo.sh index 8e2c01e760..b1eb5c01b8 100755 --- a/t/t1301-shared-repo.sh +++ b/t/t1301-shared-repo.sh @@ -137,22 +137,6 @@ test_expect_success POSIXPERM 'info/refs respects umask in unshared repo' ' test_cmp expect actual ' -test_expect_success REFFILES,POSIXPERM 'git reflog expire honors core.sharedRepository' ' - umask 077 && - git config core.sharedRepository group && - git reflog expire --all && - actual="$(ls -l .git/logs/refs/heads/main)" && - case "$actual" in - -rw-rw-*) - : happy - ;; - *) - echo Ooops, .git/logs/refs/heads/main is not 066x [$actual] - false - ;; - esac -' - test_expect_success POSIXPERM 'forced modes' ' test_when_finished "rm -rf new" && mkdir -p templates/hooks && diff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh index 78a09abc35..6ebc3ef945 100755 --- a/t/t1400-update-ref.sh +++ b/t/t1400-update-ref.sh @@ -288,33 +288,6 @@ test_expect_success "set $m (logged by touch)" ' test $A = $(git show-ref -s --verify $m) ' -test_expect_success REFFILES 'empty directory removal' ' - git branch d1/d2/r1 HEAD && - git branch d1/r2 HEAD && - test_path_is_file .git/refs/heads/d1/d2/r1 && - test_path_is_file .git/logs/refs/heads/d1/d2/r1 && - git branch -d d1/d2/r1 && - test_must_fail git show-ref --verify -q refs/heads/d1/d2 && - test_must_fail git show-ref --verify -q logs/refs/heads/d1/d2 && - test_path_is_file .git/refs/heads/d1/r2 && - test_path_is_file .git/logs/refs/heads/d1/r2 -' - -test_expect_success REFFILES 'symref empty directory removal' ' - git branch e1/e2/r1 HEAD && - git branch e1/r2 HEAD && - git checkout e1/e2/r1 && - test_when_finished "git checkout main" && - test_path_is_file .git/refs/heads/e1/e2/r1 && - test_path_is_file .git/logs/refs/heads/e1/e2/r1 && - git update-ref -d HEAD && - test_must_fail git show-ref --verify -q refs/heads/e1/e2 && - test_must_fail git show-ref --verify -q logs/refs/heads/e1/e2 && - test_path_is_file .git/refs/heads/e1/r2 && - test_path_is_file .git/logs/refs/heads/e1/r2 && - test_path_is_file .git/logs/HEAD -' - cat >expect <<EOF $Z $A $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150200 +0000 Initial Creation $A $B $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150260 +0000 Switch @@ -453,15 +426,15 @@ test_expect_success 'Query "main@{2005-05-28}" (past end of history)' ' rm -f expect git update-ref -d $m -test_expect_success REFFILES 'query reflog with gap' ' +test_expect_success 'query reflog with gap' ' test_when_finished "git update-ref -d $m" && - git update-ref $m $F && - cat >.git/logs/$m <<-EOF && - $Z $A $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150320 -0500 - $A $B $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150380 -0500 - $D $F $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150680 -0500 - EOF + GIT_COMMITTER_DATE="1117150320 -0500" git update-ref $m $A && + GIT_COMMITTER_DATE="1117150380 -0500" git update-ref $m $B && + GIT_COMMITTER_DATE="1117150480 -0500" git update-ref $m $C && + GIT_COMMITTER_DATE="1117150580 -0500" git update-ref $m $D && + GIT_COMMITTER_DATE="1117150680 -0500" git update-ref $m $F && + git reflog delete $m@{2} && git rev-parse --verify "main@{2005-05-26 23:33:01}" >actual 2>stderr && echo "$B" >expect && @@ -1668,13 +1641,4 @@ test_expect_success PIPE 'transaction flushes status updates' ' test_cmp expected actual ' -test_expect_success REFFILES 'directory not created deleting packed ref' ' - git branch d1/d2/r1 HEAD && - git pack-refs --all && - test_path_is_missing .git/refs/heads/d1/d2 && - git update-ref -d refs/heads/d1/d2/r1 && - test_path_is_missing .git/refs/heads/d1/d2 && - test_path_is_missing .git/refs/heads/d1 -' - test_done diff --git a/t/t1404-update-ref-errors.sh b/t/t1404-update-ref-errors.sh index 00b7013705..98e9158bd2 100755 --- a/t/t1404-update-ref-errors.sh +++ b/t/t1404-update-ref-errors.sh @@ -92,9 +92,6 @@ df_test() { else delname="$delref" fi && - cat >expected-err <<-EOF && - fatal: cannot lock ref $SQ$addname$SQ: $SQ$delref$SQ exists; cannot create $SQ$addref$SQ - EOF $pack && if $add_del then @@ -103,7 +100,7 @@ df_test() { printf "%s\n" "delete $delname" "create $addname $D" fi >commands && test_must_fail git update-ref --stdin <commands 2>output.err && - test_cmp expected-err output.err && + grep "fatal:\( cannot lock ref $SQ$addname$SQ:\)\? $SQ$delref$SQ exists; cannot create $SQ$addref$SQ" output.err && printf "%s\n" "$C $delref" >expected-refs && git for-each-ref --format="%(objectname) %(refname)" $prefix/r >actual-refs && test_cmp expected-refs actual-refs @@ -191,69 +188,69 @@ test_expect_success 'one new ref is a simple prefix of another' ' ' -test_expect_success REFFILES 'D/F conflict prevents add long + delete short' ' +test_expect_success 'D/F conflict prevents add long + delete short' ' df_test refs/df-al-ds --add-del foo/bar foo ' -test_expect_success REFFILES 'D/F conflict prevents add short + delete long' ' +test_expect_success 'D/F conflict prevents add short + delete long' ' df_test refs/df-as-dl --add-del foo foo/bar ' -test_expect_success REFFILES 'D/F conflict prevents delete long + add short' ' +test_expect_success 'D/F conflict prevents delete long + add short' ' df_test refs/df-dl-as --del-add foo/bar foo ' -test_expect_success REFFILES 'D/F conflict prevents delete short + add long' ' +test_expect_success 'D/F conflict prevents delete short + add long' ' df_test refs/df-ds-al --del-add foo foo/bar ' -test_expect_success REFFILES 'D/F conflict prevents add long + delete short packed' ' +test_expect_success 'D/F conflict prevents add long + delete short packed' ' df_test refs/df-al-dsp --pack --add-del foo/bar foo ' -test_expect_success REFFILES 'D/F conflict prevents add short + delete long packed' ' +test_expect_success 'D/F conflict prevents add short + delete long packed' ' df_test refs/df-as-dlp --pack --add-del foo foo/bar ' -test_expect_success REFFILES 'D/F conflict prevents delete long packed + add short' ' +test_expect_success 'D/F conflict prevents delete long packed + add short' ' df_test refs/df-dlp-as --pack --del-add foo/bar foo ' -test_expect_success REFFILES 'D/F conflict prevents delete short packed + add long' ' +test_expect_success 'D/F conflict prevents delete short packed + add long' ' df_test refs/df-dsp-al --pack --del-add foo foo/bar ' # Try some combinations involving symbolic refs... -test_expect_success REFFILES 'D/F conflict prevents indirect add long + delete short' ' +test_expect_success 'D/F conflict prevents indirect add long + delete short' ' df_test refs/df-ial-ds --sym-add --add-del foo/bar foo ' -test_expect_success REFFILES 'D/F conflict prevents indirect add long + indirect delete short' ' +test_expect_success 'D/F conflict prevents indirect add long + indirect delete short' ' df_test refs/df-ial-ids --sym-add --sym-del --add-del foo/bar foo ' -test_expect_success REFFILES 'D/F conflict prevents indirect add short + indirect delete long' ' +test_expect_success 'D/F conflict prevents indirect add short + indirect delete long' ' df_test refs/df-ias-idl --sym-add --sym-del --add-del foo foo/bar ' -test_expect_success REFFILES 'D/F conflict prevents indirect delete long + indirect add short' ' +test_expect_success 'D/F conflict prevents indirect delete long + indirect add short' ' df_test refs/df-idl-ias --sym-add --sym-del --del-add foo/bar foo ' -test_expect_success REFFILES 'D/F conflict prevents indirect add long + delete short packed' ' +test_expect_success 'D/F conflict prevents indirect add long + delete short packed' ' df_test refs/df-ial-dsp --sym-add --pack --add-del foo/bar foo ' -test_expect_success REFFILES 'D/F conflict prevents indirect add long + indirect delete short packed' ' +test_expect_success 'D/F conflict prevents indirect add long + indirect delete short packed' ' df_test refs/df-ial-idsp --sym-add --sym-del --pack --add-del foo/bar foo ' -test_expect_success REFFILES 'D/F conflict prevents add long + indirect delete short packed' ' +test_expect_success 'D/F conflict prevents add long + indirect delete short packed' ' df_test refs/df-al-idsp --sym-del --pack --add-del foo/bar foo ' -test_expect_success REFFILES 'D/F conflict prevents indirect delete long packed + indirect add short' ' +test_expect_success 'D/F conflict prevents indirect delete long packed + indirect add short' ' df_test refs/df-idlp-ias --sym-add --sym-del --pack --del-add foo/bar foo ' diff --git a/t/t1405-main-ref-store.sh b/t/t1405-main-ref-store.sh index 976bd71efb..1183232a72 100755 --- a/t/t1405-main-ref-store.sh +++ b/t/t1405-main-ref-store.sh @@ -33,12 +33,6 @@ test_expect_success 'delete_refs(FOO, refs/tags/new-tag)' ' test_must_fail git rev-parse refs/tags/new-tag -- ' -# In reftable, we keep the reflogs around for deleted refs. -test_expect_success !REFFILES 'delete-reflog(FOO, refs/tags/new-tag)' ' - $RUN delete-reflog FOO && - $RUN delete-reflog refs/tags/new-tag -' - test_expect_success 'rename_refs(main, new-main)' ' git rev-parse main >expected && $RUN rename-ref refs/heads/main refs/heads/new-main && diff --git a/t/t2011-checkout-invalid-head.sh b/t/t2011-checkout-invalid-head.sh index 3c8135831b..04f53b1ea1 100755 --- a/t/t2011-checkout-invalid-head.sh +++ b/t/t2011-checkout-invalid-head.sh @@ -29,36 +29,33 @@ test_expect_success REFFILES 'checkout notices failure to lock HEAD' ' test_must_fail git checkout -b other ' -test_expect_success REFFILES 'create ref directory/file conflict scenario' ' +test_expect_success 'create ref directory/file conflict scenario' ' git update-ref refs/heads/outer/inner main && - - # do not rely on symbolic-ref to get a known state, - # as it may use the same code we are testing reset_to_df () { - echo "ref: refs/heads/outer" >.git/HEAD + git symbolic-ref HEAD refs/heads/outer } ' -test_expect_success REFFILES 'checkout away from d/f HEAD (unpacked, to branch)' ' +test_expect_success 'checkout away from d/f HEAD (unpacked, to branch)' ' reset_to_df && git checkout main ' -test_expect_success REFFILES 'checkout away from d/f HEAD (unpacked, to detached)' ' +test_expect_success 'checkout away from d/f HEAD (unpacked, to detached)' ' reset_to_df && git checkout --detach main ' -test_expect_success REFFILES 'pack refs' ' +test_expect_success 'pack refs' ' git pack-refs --all --prune ' -test_expect_success REFFILES 'checkout away from d/f HEAD (packed, to branch)' ' +test_expect_success 'checkout away from d/f HEAD (packed, to branch)' ' reset_to_df && git checkout main ' -test_expect_success REFFILES 'checkout away from d/f HEAD (packed, to detached)' ' +test_expect_success 'checkout away from d/f HEAD (packed, to detached)' ' reset_to_df && git checkout --detach main ' diff --git a/t/t2016-checkout-patch.sh b/t/t2016-checkout-patch.sh index 747eb5563e..c4f9bf09aa 100755 --- a/t/t2016-checkout-patch.sh +++ b/t/t2016-checkout-patch.sh @@ -38,26 +38,32 @@ test_expect_success 'git checkout -p with staged changes' ' verify_state dir/foo index index ' -test_expect_success 'git checkout -p HEAD with NO staged changes: abort' ' - set_and_save_state dir/foo work head && - test_write_lines n y n | git checkout -p HEAD && - verify_saved_state bar && - verify_saved_state dir/foo -' - -test_expect_success 'git checkout -p HEAD with NO staged changes: apply' ' - test_write_lines n y y | git checkout -p HEAD && - verify_saved_state bar && - verify_state dir/foo head head -' - -test_expect_success 'git checkout -p HEAD with change already staged' ' - set_state dir/foo index index && - # the third n is to get out in case it mistakenly does not apply - test_write_lines n y n | git checkout -p HEAD && - verify_saved_state bar && - verify_state dir/foo head head -' +for opt in "HEAD" "@" +do + test_expect_success "git checkout -p $opt with NO staged changes: abort" ' + set_and_save_state dir/foo work head && + test_write_lines n y n | git checkout -p $opt >output && + verify_saved_state bar && + verify_saved_state dir/foo && + test_grep "Discard" output + ' + + test_expect_success "git checkout -p $opt with NO staged changes: apply" ' + test_write_lines n y y | git checkout -p $opt >output && + verify_saved_state bar && + verify_state dir/foo head head && + test_grep "Discard" output + ' + + test_expect_success "git checkout -p $opt with change already staged" ' + set_state dir/foo index index && + # the third n is to get out in case it mistakenly does not apply + test_write_lines n y n | git checkout -p $opt >output && + verify_saved_state bar && + verify_state dir/foo head head && + test_grep "Discard" output + ' +done test_expect_success 'git checkout -p HEAD^...' ' # the third n is to get out in case it mistakenly does not apply diff --git a/t/t2020-checkout-detach.sh b/t/t2020-checkout-detach.sh index 8202ef8c74..bce284c297 100755 --- a/t/t2020-checkout-detach.sh +++ b/t/t2020-checkout-detach.sh @@ -45,6 +45,18 @@ test_expect_success 'checkout branch does not detach' ' check_not_detached ' +for opt in "HEAD" "@" +do + test_expect_success "checkout $opt no-op/don't detach" ' + reset && + cat .git/HEAD >expect && + git checkout $opt && + cat .git/HEAD >actual && + check_not_detached && + test_cmp expect actual + ' +done + test_expect_success 'checkout tag detaches' ' reset && git checkout tag && diff --git a/t/t2024-checkout-dwim.sh b/t/t2024-checkout-dwim.sh index a97416ce65..a3b1449ef1 100755 --- a/t/t2024-checkout-dwim.sh +++ b/t/t2024-checkout-dwim.sh @@ -113,7 +113,7 @@ test_expect_success 'checkout of branch from multiple remotes fails with advice' test_grep ! "^hint: " stderr ' -test_expect_success PERL 'checkout -p with multiple remotes does not print advice' ' +test_expect_success 'checkout -p with multiple remotes does not print advice' ' git checkout -B main && test_might_fail git branch -D foo && diff --git a/t/t2071-restore-patch.sh b/t/t2071-restore-patch.sh index b5c5c0ff7e..27e85be40a 100755 --- a/t/t2071-restore-patch.sh +++ b/t/t2071-restore-patch.sh @@ -4,7 +4,7 @@ test_description='git restore --patch' . ./lib-patch-mode.sh -test_expect_success PERL 'setup' ' +test_expect_success 'setup' ' mkdir dir && echo parent >dir/foo && echo dummy >bar && @@ -16,43 +16,47 @@ test_expect_success PERL 'setup' ' save_head ' -test_expect_success PERL 'restore -p without pathspec is fine' ' +test_expect_success 'restore -p without pathspec is fine' ' echo q >cmd && git restore -p <cmd ' # note: bar sorts before dir/foo, so the first 'n' is always to skip 'bar' -test_expect_success PERL 'saying "n" does nothing' ' +test_expect_success 'saying "n" does nothing' ' set_and_save_state dir/foo work head && test_write_lines n n | git restore -p && verify_saved_state bar && verify_saved_state dir/foo ' -test_expect_success PERL 'git restore -p' ' +test_expect_success 'git restore -p' ' set_and_save_state dir/foo work head && test_write_lines n y | git restore -p && verify_saved_state bar && verify_state dir/foo head head ' -test_expect_success PERL 'git restore -p with staged changes' ' +test_expect_success 'git restore -p with staged changes' ' set_state dir/foo work index && test_write_lines n y | git restore -p && verify_saved_state bar && verify_state dir/foo index index ' -test_expect_success PERL 'git restore -p --source=HEAD' ' - set_state dir/foo work index && - # the third n is to get out in case it mistakenly does not apply - test_write_lines n y n | git restore -p --source=HEAD && - verify_saved_state bar && - verify_state dir/foo head index -' - -test_expect_success PERL 'git restore -p --source=HEAD^' ' +for opt in "HEAD" "@" +do + test_expect_success "git restore -p --source=$opt" ' + set_state dir/foo work index && + # the third n is to get out in case it mistakenly does not apply + test_write_lines n y n | git restore -p --source=$opt >output && + verify_saved_state bar && + verify_state dir/foo head index && + test_grep "Discard" output + ' +done + +test_expect_success 'git restore -p --source=HEAD^' ' set_state dir/foo work index && # the third n is to get out in case it mistakenly does not apply test_write_lines n y n | git restore -p --source=HEAD^ && @@ -60,7 +64,7 @@ test_expect_success PERL 'git restore -p --source=HEAD^' ' verify_state dir/foo parent index ' -test_expect_success PERL 'git restore -p --source=HEAD^...' ' +test_expect_success 'git restore -p --source=HEAD^...' ' set_state dir/foo work index && # the third n is to get out in case it mistakenly does not apply test_write_lines n y n | git restore -p --source=HEAD^... && @@ -68,7 +72,7 @@ test_expect_success PERL 'git restore -p --source=HEAD^...' ' verify_state dir/foo parent index ' -test_expect_success PERL 'git restore -p handles deletion' ' +test_expect_success 'git restore -p handles deletion' ' set_state dir/foo work index && rm dir/foo && test_write_lines n y | git restore -p && @@ -81,21 +85,21 @@ test_expect_success PERL 'git restore -p handles deletion' ' # dir/foo. There's always an extra 'n' to reject edits to dir/foo in # the failure case (and thus get out of the loop). -test_expect_success PERL 'path limiting works: dir' ' +test_expect_success 'path limiting works: dir' ' set_state dir/foo work head && test_write_lines y n | git restore -p dir && verify_saved_state bar && verify_state dir/foo head head ' -test_expect_success PERL 'path limiting works: -- dir' ' +test_expect_success 'path limiting works: -- dir' ' set_state dir/foo work head && test_write_lines y n | git restore -p -- dir && verify_saved_state bar && verify_state dir/foo head head ' -test_expect_success PERL 'path limiting works: HEAD^ -- dir' ' +test_expect_success 'path limiting works: HEAD^ -- dir' ' set_state dir/foo work head && # the third n is to get out in case it mistakenly does not apply test_write_lines y n n | git restore -p --source=HEAD^ -- dir && @@ -103,7 +107,7 @@ test_expect_success PERL 'path limiting works: HEAD^ -- dir' ' verify_state dir/foo parent head ' -test_expect_success PERL 'path limiting works: foo inside dir' ' +test_expect_success 'path limiting works: foo inside dir' ' set_state dir/foo work head && # the third n is to get out in case it mistakenly does not apply test_write_lines y n n | (cd dir && git restore -p foo) && @@ -111,7 +115,7 @@ test_expect_success PERL 'path limiting works: foo inside dir' ' verify_state dir/foo head head ' -test_expect_success PERL 'none of this moved HEAD' ' +test_expect_success 'none of this moved HEAD' ' verify_saved_head ' diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh index de7d3014e4..e36f4d15f2 100755 --- a/t/t3200-branch.sh +++ b/t/t3200-branch.sh @@ -836,35 +836,6 @@ test_expect_success 'renaming a symref is not allowed' ' test_ref_missing refs/heads/new-topic ' -test_expect_success SYMLINKS,REFFILES 'git branch -m u v should fail when the reflog for u is a symlink' ' - git branch --create-reflog u && - mv .git/logs/refs/heads/u real-u && - ln -s real-u .git/logs/refs/heads/u && - test_must_fail git branch -m u v -' - -test_expect_success SYMLINKS,REFFILES 'git branch -m with symlinked .git/refs' ' - test_when_finished "rm -rf subdir" && - git init --bare subdir && - - rm -rfv subdir/refs subdir/objects subdir/packed-refs && - ln -s ../.git/refs subdir/refs && - ln -s ../.git/objects subdir/objects && - ln -s ../.git/packed-refs subdir/packed-refs && - - git -C subdir rev-parse --absolute-git-dir >subdir.dir && - git rev-parse --absolute-git-dir >our.dir && - ! test_cmp subdir.dir our.dir && - - git -C subdir log && - git -C subdir branch rename-src && - git rev-parse rename-src >expect && - git -C subdir branch -m rename-src rename-dest && - git rev-parse rename-dest >actual && - test_cmp expect actual && - git branch -D rename-dest -' - test_expect_success 'test tracking setup via --track' ' git config remote.local.url . && git config remote.local.fetch refs/heads/*:refs/remotes/local/* && diff --git a/t/t3400-rebase.sh b/t/t3400-rebase.sh index 57f1392926..e1c8c5f701 100755 --- a/t/t3400-rebase.sh +++ b/t/t3400-rebase.sh @@ -424,16 +424,6 @@ test_expect_success 'refuse to switch to branch checked out elsewhere' ' test_grep "already used by worktree at" err ' -test_expect_success REFFILES,MINGW,SYMLINKS_WINDOWS 'rebase when .git/logs is a symlink' ' - git checkout main && - mv .git/logs actual_logs && - cmd //c "mklink /D .git\logs ..\actual_logs" && - git rebase -f HEAD^ && - test -L .git/logs && - rm .git/logs && - mv actual_logs .git/logs -' - test_expect_success 'rebase when inside worktree subdirectory' ' git init main-wt && ( diff --git a/t/t3904-stash-patch.sh b/t/t3904-stash-patch.sh index accfe3845c..368fc2a6cc 100755 --- a/t/t3904-stash-patch.sh +++ b/t/t3904-stash-patch.sh @@ -3,12 +3,6 @@ test_description='stash -p' . ./lib-patch-mode.sh -if ! test_have_prereq PERL -then - skip_all='skipping stash -p tests, perl not available' - test_done -fi - test_expect_success 'setup' ' mkdir dir && echo parent > dir/foo && diff --git a/t/t4129-apply-samemode.sh b/t/t4129-apply-samemode.sh index 2775bfadd8..4eb8444029 100755 --- a/t/t4129-apply-samemode.sh +++ b/t/t4129-apply-samemode.sh @@ -103,4 +103,31 @@ test_expect_success POSIXPERM 'do not use core.sharedRepository for working tree ) ' +test_expect_success 'git apply respects core.fileMode' ' + test_config core.fileMode false && + echo true >script.sh && + git add --chmod=+x script.sh && + git ls-files -s script.sh >ls-files-output && + test_grep "^100755" ls-files-output && + test_tick && git commit -m "Add script" && + git ls-tree -r HEAD script.sh >ls-tree-output && + test_grep "^100755" ls-tree-output && + + echo true >>script.sh && + test_tick && git commit -m "Modify script" script.sh && + git format-patch -1 --stdout >patch && + test_grep "^index.*100755$" patch && + + git switch -c branch HEAD^ && + git apply --index patch 2>err && + test_grep ! "has type 100644, expected 100755" err && + git reset --hard && + + git apply patch 2>err && + test_grep ! "has type 100644, expected 100755" err && + + git apply --cached patch 2>err && + test_grep ! "has type 100644, expected 100755" err +' + test_done diff --git a/t/t7003-filter-branch.sh b/t/t7003-filter-branch.sh index f6aebe92ff..5ab4d41ee7 100755 --- a/t/t7003-filter-branch.sh +++ b/t/t7003-filter-branch.sh @@ -396,10 +396,7 @@ test_expect_success '--prune-empty is able to prune entire branch' ' git branch prune-entire B && git filter-branch -f --prune-empty --index-filter "git update-index --remove A.t B.t" prune-entire && test_must_fail git rev-parse refs/heads/prune-entire && - if test_have_prereq REFFILES - then - test_must_fail git reflog exists refs/heads/prune-entire - fi + test_must_fail git reflog exists refs/heads/prune-entire ' test_expect_success '--remap-to-ancestor with filename filters' ' diff --git a/t/t7105-reset-patch.sh b/t/t7105-reset-patch.sh index 05079c7246..f4f3b7a677 100755 --- a/t/t7105-reset-patch.sh +++ b/t/t7105-reset-patch.sh @@ -5,7 +5,7 @@ test_description='git reset --patch' TEST_PASSES_SANITIZE_LEAK=true . ./lib-patch-mode.sh -test_expect_success PERL 'setup' ' +test_expect_success 'setup' ' mkdir dir && echo parent > dir/foo && echo dummy > bar && @@ -19,42 +19,46 @@ test_expect_success PERL 'setup' ' # note: bar sorts before foo, so the first 'n' is always to skip 'bar' -test_expect_success PERL 'saying "n" does nothing' ' +test_expect_success 'saying "n" does nothing' ' set_and_save_state dir/foo work work && test_write_lines n n | git reset -p && verify_saved_state dir/foo && verify_saved_state bar ' -test_expect_success PERL 'git reset -p' ' - test_write_lines n y | git reset -p >output && - verify_state dir/foo work head && - verify_saved_state bar && - test_grep "Unstage" output -' - -test_expect_success PERL 'git reset -p HEAD^' ' +for opt in "HEAD" "@" "" +do + test_expect_success "git reset -p $opt" ' + set_and_save_state dir/foo work work && + test_write_lines n y | git reset -p $opt >output && + verify_state dir/foo work head && + verify_saved_state bar && + test_grep "Unstage" output + ' +done + +test_expect_success 'git reset -p HEAD^' ' test_write_lines n y | git reset -p HEAD^ >output && verify_state dir/foo work parent && verify_saved_state bar && test_grep "Apply" output ' -test_expect_success PERL 'git reset -p HEAD^^{tree}' ' +test_expect_success 'git reset -p HEAD^^{tree}' ' test_write_lines n y | git reset -p HEAD^^{tree} >output && verify_state dir/foo work parent && verify_saved_state bar && test_grep "Apply" output ' -test_expect_success PERL 'git reset -p HEAD^:dir/foo (blob fails)' ' +test_expect_success 'git reset -p HEAD^:dir/foo (blob fails)' ' set_and_save_state dir/foo work work && test_must_fail git reset -p HEAD^:dir/foo && verify_saved_state dir/foo && verify_saved_state bar ' -test_expect_success PERL 'git reset -p aaaaaaaa (unknown fails)' ' +test_expect_success 'git reset -p aaaaaaaa (unknown fails)' ' set_and_save_state dir/foo work work && test_must_fail git reset -p aaaaaaaa && verify_saved_state dir/foo && @@ -66,27 +70,27 @@ test_expect_success PERL 'git reset -p aaaaaaaa (unknown fails)' ' # dir/foo. There's always an extra 'n' to reject edits to dir/foo in # the failure case (and thus get out of the loop). -test_expect_success PERL 'git reset -p dir' ' +test_expect_success 'git reset -p dir' ' set_state dir/foo work work && test_write_lines y n | git reset -p dir && verify_state dir/foo work head && verify_saved_state bar ' -test_expect_success PERL 'git reset -p -- foo (inside dir)' ' +test_expect_success 'git reset -p -- foo (inside dir)' ' set_state dir/foo work work && test_write_lines y n | (cd dir && git reset -p -- foo) && verify_state dir/foo work head && verify_saved_state bar ' -test_expect_success PERL 'git reset -p HEAD^ -- dir' ' +test_expect_success 'git reset -p HEAD^ -- dir' ' test_write_lines y n | git reset -p HEAD^ -- dir && verify_state dir/foo work parent && verify_saved_state bar ' -test_expect_success PERL 'none of this moved HEAD' ' +test_expect_success 'none of this moved HEAD' ' verify_saved_head ' diff --git a/t/t7106-reset-unborn-branch.sh b/t/t7106-reset-unborn-branch.sh index d20e5709f9..88d1c8adf4 100755 --- a/t/t7106-reset-unborn-branch.sh +++ b/t/t7106-reset-unborn-branch.sh @@ -34,7 +34,7 @@ test_expect_success 'reset $file' ' test_cmp expect actual ' -test_expect_success PERL 'reset -p' ' +test_expect_success 'reset -p' ' rm .git/index && git add a && echo y >yes && diff --git a/t/t7514-commit-patch.sh b/t/t7514-commit-patch.sh index 998a2103c7..b4de10a5dd 100755 --- a/t/t7514-commit-patch.sh +++ b/t/t7514-commit-patch.sh @@ -3,12 +3,6 @@ test_description='hunk edit with "commit -p -m"' . ./test-lib.sh -if ! test_have_prereq PERL -then - skip_all="skipping '$test_description' tests, perl not available" - test_done -fi - test_expect_success 'setup (initial)' ' echo line1 >file && git add file && diff --git a/t/t7800-difftool.sh b/t/t7800-difftool.sh index 6a36be1e63..96ae5d5880 100755 --- a/t/t7800-difftool.sh +++ b/t/t7800-difftool.sh @@ -91,58 +91,67 @@ test_expect_success 'difftool forwards arguments to diff' ' rm for-diff ' -test_expect_success 'difftool ignores exit code' ' - test_config difftool.error.cmd false && - git difftool -y -t error branch -' +for opt in '' '--dir-diff' +do + test_expect_success "difftool ${opt} ignores exit code" " + test_config difftool.error.cmd false && + git difftool ${opt} -y -t error branch + " -test_expect_success 'difftool forwards exit code with --trust-exit-code' ' - test_config difftool.error.cmd false && - test_must_fail git difftool -y --trust-exit-code -t error branch -' + test_expect_success "difftool ${opt} forwards exit code with --trust-exit-code" " + test_config difftool.error.cmd false && + test_must_fail git difftool ${opt} -y --trust-exit-code -t error branch + " -test_expect_success 'difftool forwards exit code with --trust-exit-code for built-ins' ' - test_config difftool.vimdiff.path false && - test_must_fail git difftool -y --trust-exit-code -t vimdiff branch -' + test_expect_success "difftool ${opt} forwards exit code with --trust-exit-code for built-ins" " + test_config difftool.vimdiff.path false && + test_must_fail git difftool ${opt} -y --trust-exit-code -t vimdiff branch + " -test_expect_success 'difftool honors difftool.trustExitCode = true' ' - test_config difftool.error.cmd false && - test_config difftool.trustExitCode true && - test_must_fail git difftool -y -t error branch -' + test_expect_success "difftool ${opt} honors difftool.trustExitCode = true" " + test_config difftool.error.cmd false && + test_config difftool.trustExitCode true && + test_must_fail git difftool ${opt} -y -t error branch + " -test_expect_success 'difftool honors difftool.trustExitCode = false' ' - test_config difftool.error.cmd false && - test_config difftool.trustExitCode false && - git difftool -y -t error branch -' + test_expect_success "difftool ${opt} honors difftool.trustExitCode = false" " + test_config difftool.error.cmd false && + test_config difftool.trustExitCode false && + git difftool ${opt} -y -t error branch + " -test_expect_success 'difftool ignores exit code with --no-trust-exit-code' ' - test_config difftool.error.cmd false && - test_config difftool.trustExitCode true && - git difftool -y --no-trust-exit-code -t error branch -' + test_expect_success "difftool ${opt} ignores exit code with --no-trust-exit-code" " + test_config difftool.error.cmd false && + test_config difftool.trustExitCode true && + git difftool ${opt} -y --no-trust-exit-code -t error branch + " -test_expect_success 'difftool stops on error with --trust-exit-code' ' - test_when_finished "rm -f for-diff .git/fail-right-file" && - test_when_finished "git reset -- for-diff" && - write_script .git/fail-right-file <<-\EOF && - echo failed - exit 1 - EOF - >for-diff && - git add for-diff && - test_must_fail git difftool -y --trust-exit-code \ - --extcmd .git/fail-right-file branch >actual && - test_line_count = 1 actual -' + test_expect_success "difftool ${opt} stops on error with --trust-exit-code" " + test_when_finished 'rm -f for-diff .git/fail-right-file' && + test_when_finished 'git reset -- for-diff' && + write_script .git/fail-right-file <<-\EOF && + echo failed + exit 1 + EOF + >for-diff && + git add for-diff && + test_must_fail git difftool ${opt} -y --trust-exit-code \ + --extcmd .git/fail-right-file branch >actual && + test_line_count = 1 actual + " -test_expect_success 'difftool honors exit status if command not found' ' - test_config difftool.nonexistent.cmd i-dont-exist && - test_config difftool.trustExitCode false && - test_must_fail git difftool -y -t nonexistent branch -' + test_expect_success "difftool ${opt} honors exit status if command not found" " + test_config difftool.nonexistent.cmd i-dont-exist && + test_config difftool.trustExitCode false && + if test "${opt}" = '--dir-diff' + then + expected_code=127 + else + expected_code=128 + fi && + test_expect_code \${expected_code} git difftool ${opt} -y -t nonexistent branch + " +done test_expect_success 'difftool honors --gui' ' difftool_test_setup && diff --git a/t/t9002-column.sh b/t/t9002-column.sh index 348cc40658..d5b98e615b 100755 --- a/t/t9002-column.sh +++ b/t/t9002-column.sh @@ -196,4 +196,15 @@ EOF test_cmp expected actual ' +test_expect_success 'padding must be non-negative' ' + cat >input <<\EOF && +1 2 3 4 5 6 +EOF + cat >expected <<\EOF && +fatal: --padding must be non-negative +EOF + test_must_fail git column --mode=column --padding=-1 <input >actual 2>&1 && + test_cmp expected actual +' + test_done diff --git a/t/t9146-git-svn-empty-dirs.sh b/t/t9146-git-svn-empty-dirs.sh index 09606f1b3c..926ac81439 100755 --- a/t/t9146-git-svn-empty-dirs.sh +++ b/t/t9146-git-svn-empty-dirs.sh @@ -20,11 +20,7 @@ test_expect_success 'empty directories exist' ' cd cloned && for i in a b c d d/e d/e/f "weird file name" do - if ! test -d "$i" - then - echo >&2 "$i does not exist" && - exit 1 - fi + test_path_is_dir "$i" || exit 1 done ) ' @@ -37,11 +33,7 @@ test_expect_success 'option automkdirs set to false' ' git svn fetch && for i in a b c d d/e d/e/f "weird file name" do - if test -d "$i" - then - echo >&2 "$i exists" && - exit 1 - fi + test_path_is_missing "$i" || exit 1 done ) ' @@ -52,7 +44,7 @@ test_expect_success 'more emptiness' ' test_expect_success 'git svn rebase creates empty directory' ' ( cd cloned && git svn rebase ) && - test -d cloned/"! !" + test_path_is_dir cloned/"! !" ' test_expect_success 'git svn mkdirs recreates empty directories' ' @@ -62,11 +54,7 @@ test_expect_success 'git svn mkdirs recreates empty directories' ' git svn mkdirs && for i in a b c d d/e d/e/f "weird file name" "! !" do - if ! test -d "$i" - then - echo >&2 "$i does not exist" && - exit 1 - fi + test_path_is_dir "$i" || exit 1 done ) ' @@ -78,25 +66,13 @@ test_expect_success 'git svn mkdirs -r works' ' git svn mkdirs -r7 && for i in a b c d d/e d/e/f "weird file name" do - if ! test -d "$i" - then - echo >&2 "$i does not exist" && - exit 1 - fi + test_path_is_dir "$i" || exit 1 done && - if test -d "! !" - then - echo >&2 "$i should not exist" && - exit 1 - fi && + test_path_is_missing "! !" || exit 1 && git svn mkdirs -r8 && - if ! test -d "! !" - then - echo >&2 "$i not exist" && - exit 1 - fi + test_path_is_dir "! !" || exit 1 ) ' @@ -114,11 +90,7 @@ test_expect_success 'empty directories in trunk exist' ' cd trunk && for i in a "weird file name" do - if ! test -d "$i" - then - echo >&2 "$i does not exist" && - exit 1 - fi + test_path_is_dir "$i" || exit 1 done ) ' @@ -129,7 +101,7 @@ test_expect_success 'remove a top-level directory from svn' ' test_expect_success 'removed top-level directory does not exist' ' git svn clone "$svnrepo" removed && - test ! -e removed/d + test_path_is_missing removed/d ' unhandled=.git/svn/refs/remotes/git-svn/unhandled.log @@ -143,15 +115,11 @@ test_expect_success 'git svn gc-ed files work' ' svn_cmd mkdir -m gz "$svnrepo"/gz && git reset --hard $(git rev-list HEAD | tail -1) && git svn rebase && - test -f "$unhandled".gz && - test -f "$unhandled" && + test_path_is_file "$unhandled".gz && + test_path_is_file "$unhandled" && for i in a b c "weird file name" gz "! !" do - if ! test -d "$i" - then - echo >&2 "$i does not exist" && - exit 1 - fi + test_path_is_dir "$i" || exit 1 done fi ) diff --git a/t/t9210-scalar.sh b/t/t9210-scalar.sh index 4432a30d10..428339e342 100755 --- a/t/t9210-scalar.sh +++ b/t/t9210-scalar.sh @@ -154,7 +154,14 @@ test_expect_success 'scalar clone' ' test_cmp expect actual && test_path_is_missing 1/2 && - test_must_fail git rev-list --missing=print $second && + + # This relies on the fact that the presence of "--missing" + # on the command line forces lazy fetching off before + # "$second^{blob}" gets parsed. Without "^{blob}", a + # bare object name "$second" is taken into the queue and + # the command may not fail with a fixed "rev-list --missing". + test_must_fail git rev-list --missing=print "$second^{blob}" -- && + git rev-list $second && git cat-file blob $second >actual && echo "second" >expect && diff --git a/t/test-lib.sh b/t/test-lib.sh index 042f557a6f..c8af8dab79 100644 --- a/t/test-lib.sh +++ b/t/test-lib.sh @@ -1755,6 +1755,8 @@ esac case "$GIT_DEFAULT_REF_FORMAT" in files) test_set_prereq REFFILES;; +reftable) + test_set_prereq REFTABLE;; *) echo 2>&1 "error: unknown ref format $GIT_DEFAULT_REF_FORMAT" exit 1 diff --git a/userdiff.c b/userdiff.c index e399543823..2b1dab2649 100644 --- a/userdiff.c +++ b/userdiff.c @@ -323,8 +323,7 @@ static int userdiff_find_by_namelen_cb(struct userdiff_driver *driver, { struct find_by_namelen_data *cb_data = priv; - if (!strncmp(driver->name, cb_data->name, cb_data->len) && - !driver->name[cb_data->len]) { + if (!xstrncmpz(driver->name, cb_data->name, cb_data->len)) { cb_data->driver = driver; return 1; /* tell the caller to stop iterating */ } |
